├── .npmignore ├── testutils.d.ts ├── .gitignore ├── src ├── testutils │ ├── index.ts │ ├── ShouldThrowError.ts │ ├── redis.ts │ ├── RedisTestMonitor.ts │ └── TestDecorators.ts ├── Subscriber │ ├── EntitySubscriberInterface.ts │ └── PubSubSubscriberInterface.ts ├── Decorators │ ├── __tests__ │ │ ├── Entity_Spec.ts │ │ ├── __snapshots__ │ │ │ ├── IdentifyProperty_Spec.ts.snap │ │ │ ├── RelationProperty_Spec.ts.snap │ │ │ └── Property_Spec.ts.snap │ │ ├── IdentifyProperty_Spec.ts │ │ ├── Property_Spec.ts │ │ └── RelationProperty_Spec.ts │ ├── Entity.ts │ ├── IdentifyProperty.ts │ ├── Property.ts │ └── RelationProperty.ts ├── Errors │ └── Errors.ts ├── utils │ ├── hasPrototypeOf.ts │ ├── __tests__ │ │ ├── Container_Spec.ts │ │ └── hasPrototypeOf_Spec.ts │ ├── Container.ts │ └── PromisedRedis.ts ├── Collections │ ├── __tests__ │ │ ├── LazySet_Spec.ts │ │ ├── LazyMap_Spec.ts │ │ ├── RedisLazySet_Spec.ts │ │ └── RedisLazyMap_Spec.ts │ ├── LazySet.ts │ ├── LazyMap.ts │ ├── RedisLazySet.ts │ └── RedisLazyMap.ts ├── Metadata │ ├── __tests__ │ │ ├── __snapshots__ │ │ │ └── Metadata_Spec.ts.snap │ │ └── Metadata_Spec.ts │ └── Metadata.ts ├── index.ts ├── Persistence │ ├── __tests__ │ │ └── __snapshots__ │ │ │ └── RedisManager_Spec.ts.snap │ └── RedisManager.ts └── Connection │ ├── Connection.ts │ └── __tests__ │ └── Connection_Spec.ts ├── docker-compose.yml ├── types └── sb-promisify │ └── index.d.ts ├── testutils.js ├── tsconfig-build.json ├── .vscode ├── settings.json └── launch.json ├── tsconfig.json ├── package.json ├── tslint.json └── Readme.md /.npmignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | helpers 3 | node_modules -------------------------------------------------------------------------------- /testutils.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./compiled/testutils"; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | compiled 2 | node_modules 3 | **/*.js 4 | **/*.map 5 | !testutils.js -------------------------------------------------------------------------------- /src/testutils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./redis"; 2 | export * from "./RedisTestMonitor"; -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.1' 2 | 3 | services: 4 | redis: 5 | image: redis:3-alpine 6 | ports: 7 | - 6379:6379 8 | restart: on-failure -------------------------------------------------------------------------------- /src/testutils/ShouldThrowError.ts: -------------------------------------------------------------------------------- 1 | 2 | export class ShouldThrowError extends Error { 3 | public constructor() { 4 | super("The test should throw"); 5 | } 6 | } -------------------------------------------------------------------------------- /types/sb-promisify/index.d.ts: -------------------------------------------------------------------------------- 1 | export function promisify(callback: Function, throwError?: boolean): Promise; 2 | export function promisifyAll(object: T, throwError?: boolean): T; -------------------------------------------------------------------------------- /testutils.js: -------------------------------------------------------------------------------- 1 | function __export(m) { 2 | for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; 3 | } 4 | Object.defineProperty(exports, "__esModule", { value: true }); 5 | 6 | __export(require("./compiled/testutils")); -------------------------------------------------------------------------------- /src/Subscriber/EntitySubscriberInterface.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface EntitySubscriberInterface { 3 | listenTo(): Function; 4 | 5 | beforeSave?(entity: T): Promise; 6 | 7 | afterSave?(entity: T): Promise; 8 | 9 | afterLoad?(entity: T): Promise; 10 | 11 | beforeRemove?(entity: T): Promise; 12 | 13 | afterRemove?(entity: T): Promise; 14 | } -------------------------------------------------------------------------------- /src/Decorators/__tests__/Entity_Spec.ts: -------------------------------------------------------------------------------- 1 | import { REDIS_ENTITY } from "../../Metadata/Metadata"; 2 | import { Entity } from "../Entity"; 3 | 4 | @Entity() 5 | class Test {} 6 | 7 | @Entity("somename") 8 | class Test2 { } 9 | 10 | it("Defines metadata", () => { 11 | expect(Reflect.getMetadata(REDIS_ENTITY, Test)).toBe("Test"); 12 | expect(Reflect.getMetadata(REDIS_ENTITY, Test2)).toBe("somename"); 13 | }); -------------------------------------------------------------------------------- /src/Decorators/Entity.ts: -------------------------------------------------------------------------------- 1 | import { REDIS_ENTITY } from "../Metadata/Metadata"; 2 | 3 | /** 4 | * Defines redis entity class 5 | * 6 | * @export 7 | * @param {string} [name] 8 | * @returns void 9 | */ 10 | export function Entity(name?: string): ClassDecorator { 11 | return function (constructor: Function) { 12 | Reflect.defineMetadata(REDIS_ENTITY, name ? name : constructor.name, constructor); 13 | }; 14 | } -------------------------------------------------------------------------------- /src/Errors/Errors.ts: -------------------------------------------------------------------------------- 1 | export class AlreadyConnectedError extends Error { 2 | public constructor() { 3 | super("Already connected to Redis"); 4 | } 5 | } 6 | 7 | export class MetadataError extends Error { 8 | public constructor(entity: Function, msg: string) { 9 | super(`Metadata error for ${entity.name}: ${msg}`); 10 | } 11 | } 12 | 13 | export class DuplicateIdsInEntityError extends Error { 14 | public constructor(entity: object, id: string) { 15 | super(`Entity of type ${entity.constructor.name} with same IDs: ${id} but with different object links were found`); 16 | } 17 | } -------------------------------------------------------------------------------- /src/utils/hasPrototypeOf.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Check if given function constructor has given prototype in inheritance chain 4 | * 5 | * @export 6 | * @template TProto 7 | * @param cl 8 | * @param checkProto 9 | * @returns 10 | */ 11 | export function hasPrototypeOf(cl: Function | undefined, checkProto: TProto): cl is TProto { 12 | if (!cl) { 13 | return false; 14 | } 15 | let prototype = cl.prototype; 16 | while (prototype) { 17 | if (prototype === checkProto.prototype) { return true; } 18 | prototype = Object.getPrototypeOf(prototype); 19 | } 20 | return false; 21 | } -------------------------------------------------------------------------------- /src/Decorators/__tests__/__snapshots__/IdentifyProperty_Spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Defines metadata with custom name 1`] = ` 4 | Array [ 5 | Object { 6 | "isIdentifyColumn": true, 7 | "isRelation": false, 8 | "propertyName": "id", 9 | "propertyRedisName": "somename", 10 | "propertyType": [Function], 11 | }, 12 | ] 13 | `; 14 | 15 | exports[`Defines metadata without custom name 1`] = ` 16 | Array [ 17 | Object { 18 | "isIdentifyColumn": true, 19 | "isRelation": false, 20 | "propertyName": "id", 21 | "propertyRedisName": "id", 22 | "propertyType": [Function], 23 | }, 24 | ] 25 | `; 26 | -------------------------------------------------------------------------------- /src/utils/__tests__/Container_Spec.ts: -------------------------------------------------------------------------------- 1 | import { ContainerInterface, getFromContainer, useContainer } from "../Container"; 2 | 3 | class Test {} 4 | 5 | it("Should create instance from default container", () => { 6 | const t = getFromContainer(Test); 7 | expect(t).toBeInstanceOf(Test); 8 | 9 | const k = getFromContainer(Test); 10 | expect(k).toBe(t); 11 | }); 12 | 13 | it("Should create instance from user container", () => { 14 | const container: ContainerInterface = { 15 | get: jest.fn().mockImplementation((cl: any) => new cl()) 16 | }; 17 | useContainer(container); 18 | 19 | const ins = getFromContainer(Test); 20 | expect(ins).toBeInstanceOf(Test); 21 | expect(container.get).toBeCalledWith(Test); 22 | }); 23 | -------------------------------------------------------------------------------- /tsconfig-build.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "extends": "./tsconfig", 4 | "compilerOptions": { 5 | "inlineSourceMap": false, 6 | "declaration": true, 7 | "sourceMap": true, 8 | "noEmit": false, 9 | "types": [ 10 | "reflect-metadata" 11 | ], 12 | "typeRoots": [ 13 | "node_modules/types", 14 | "types" 15 | ], 16 | "baseUrl": ".", 17 | "paths": { 18 | "sb-promisify": [ 19 | "./types/sb-promisify" 20 | ] 21 | }, 22 | "outDir": "./compiled" 23 | }, 24 | "include": [ 25 | "src/**/*" 26 | ], 27 | "files": [ 28 | "src/testutils/redis.ts", 29 | "src/testutils/index.ts", 30 | "src/testutils/RedisTestMonitor" 31 | ], 32 | "exclude": [ 33 | "**/*_Spec.*", 34 | "src/testutils" 35 | ] 36 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "tsimporter.doubleQuotes": true, 4 | "tslint.autoFixOnSave": true, 5 | "tslint.alwaysShowRuleFailuresAsWarnings": true, 6 | "docthis.includeTypes": false, 7 | "docthis.includeMemberOfOnClassMembers": false, 8 | "docthis.includeMemberOfOnInterfaceMembers": false, 9 | "docthis.includeDescriptionTag": false, 10 | "runCurrentTest.run": "npm run test -- ${relativeTestPath} --testNamePattern \"${fullTestName}\"", 11 | "runCurrentTest.runAndUpdateSnapshots": "npm run test -- -u ${relativeTestPath} --testNamePattern \"${fullTestName}\"", 12 | "search.exclude": { 13 | "**/node_modules": true, 14 | "**/bower_components": true, 15 | "compiled": true 16 | }, 17 | "snapshotTools.testFileExt": [ 18 | ".tsx", 19 | ".ts" 20 | ], 21 | "tsimporter.filesToExclude": [ 22 | "./compiled/**/*" 23 | ] 24 | } -------------------------------------------------------------------------------- /src/Collections/__tests__/LazySet_Spec.ts: -------------------------------------------------------------------------------- 1 | import { LazySet } from "../LazySet"; 2 | 3 | it("Ordinary set but with promised methods", async () => { 4 | const set = new LazySet(); 5 | await set.add(1); 6 | expect(await set.has(1)).toBeTruthy(); 7 | expect(await set.size()).toBe(1); 8 | await set.add(2); 9 | await set.add(3); 10 | expect(await set.size()).toBe(3); 11 | await set.delete(3); 12 | expect(await set.size()).toBe(2); 13 | 14 | for await (const val of set.values()) { 15 | expect(val).toEqual(expect.any(Number)); 16 | } 17 | const iterated = []; 18 | for await (const val of set.values()) { 19 | expect(val).toEqual(expect.any(Number)); 20 | iterated.push(val); 21 | } 22 | expect(iterated).toEqual(expect.arrayContaining([1, 2])); 23 | 24 | let values = await set.toArray(); 25 | expect(values).toEqual(expect.arrayContaining([1, 2])); 26 | values = await set.toArray(); 27 | expect(values).toEqual(expect.arrayContaining([1, 2])); 28 | }); -------------------------------------------------------------------------------- /src/utils/Container.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Service container interface 3 | * 4 | * @export 5 | * @interface ContainerInterface 6 | */ 7 | export interface ContainerInterface { 8 | get(cl: { new(...args: any[]): T }): T; 9 | } 10 | 11 | const defaultContainerInstances: Map = new Map(); 12 | 13 | let usedContainer: ContainerInterface = { 14 | get: cl => { 15 | let ins = defaultContainerInstances.get(cl); 16 | if (!ins) { 17 | ins = new cl(); 18 | defaultContainerInstances.set(cl, ins); 19 | } 20 | return ins; 21 | } 22 | }; 23 | 24 | /** 25 | * Set up service container to use 26 | * 27 | * @export 28 | * @param container 29 | */ 30 | export function useContainer(container: ContainerInterface): void { 31 | usedContainer = container; 32 | } 33 | 34 | /** 35 | * Return class instance from container 36 | * 37 | * @export 38 | * @template T 39 | * @param cl 40 | * @returns 41 | */ 42 | export function getFromContainer(cl: { new(...args: any[]): T }): T { 43 | return usedContainer.get(cl); 44 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "experimentalDecorators": true, 5 | "emitDecoratorMetadata": true, 6 | "module": "commonjs", 7 | "target": "es2016", 8 | "lib": [ 9 | "es2015", 10 | "es2016", 11 | "esnext.asynciterable" 12 | ], 13 | "strictNullChecks": true, 14 | "declaration": false, 15 | "noEmitOnError": true, 16 | "noEmit": true, 17 | "noUnusedLocals": true, 18 | "sourceMap": false, 19 | "inlineSourceMap": true, 20 | "types": [ 21 | "reflect-metadata", 22 | "jest" 23 | ], 24 | "typeRoots": [ 25 | "node_modules/types", 26 | "types" 27 | ], 28 | "baseUrl": ".", 29 | "paths": { 30 | "sb-promisify": [ 31 | "./types/sb-promisify" 32 | ] 33 | } 34 | }, 35 | "include": [ 36 | "src/**/*" 37 | ], 38 | "exclude": [ 39 | "node_modules", 40 | "compiled" 41 | ] 42 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "protocol": "inspector", 11 | "name": "Debug current test file", 12 | "console": "integratedTerminal", 13 | "cwd": "${workspaceRoot}", 14 | "runtimeArgs": [ 15 | "--inspect-brk", 16 | "./node_modules/.bin/jest", 17 | "${relativeFile}", 18 | "-i", 19 | "--env", 20 | "jest-environment-node-debug" 21 | ], 22 | "env": { 23 | "REDIS_HOST": "localhost", 24 | "REDIS_PORT": "6379" 25 | }, 26 | "sourceMaps": true, 27 | "showAsyncStacks": true, 28 | "smartStep": true 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /src/Metadata/__tests__/__snapshots__/Metadata_Spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`getRedisHashProperties() Returns properties for even hash instance or hash constructor 1`] = ` 4 | Array [ 5 | Object { 6 | "isIdentifyColumn": true, 7 | "isRelation": false, 8 | "propertyName": "id", 9 | "propertyRedisName": "id", 10 | "propertyType": [Function], 11 | }, 12 | Object { 13 | "isIdentifyColumn": false, 14 | "isRelation": false, 15 | "propertyName": "prop2", 16 | "propertyRedisName": "prop2", 17 | "propertyType": [Function], 18 | }, 19 | ] 20 | `; 21 | 22 | exports[`getRedisHashProperties() Returns properties for even hash instance or hash constructor 2`] = ` 23 | Array [ 24 | Object { 25 | "isIdentifyColumn": true, 26 | "isRelation": false, 27 | "propertyName": "id", 28 | "propertyRedisName": "id", 29 | "propertyType": [Function], 30 | }, 31 | Object { 32 | "isIdentifyColumn": false, 33 | "isRelation": false, 34 | "propertyName": "prop2", 35 | "propertyRedisName": "prop2", 36 | "propertyType": [Function], 37 | }, 38 | ] 39 | `; 40 | -------------------------------------------------------------------------------- /src/Collections/__tests__/LazyMap_Spec.ts: -------------------------------------------------------------------------------- 1 | import { LazyMap } from "../LazyMap"; 2 | 3 | it("It's ordinary promised map", async () => { 4 | const map = new LazyMap(); 5 | await map.set(1, "test"); 6 | await map.set(2, "test2"); 7 | expect(await map.size()).toBe(2); 8 | expect(await map.get(1)).toBe("test"); 9 | expect(await map.has(1)).toBeTruthy(); 10 | await map.delete(2); 11 | expect(await map.has(2)).toBeFalsy(); 12 | expect(await map.size()).toBe(1); 13 | 14 | await map.set(2, "test2"); 15 | await map.set(3, "test3"); 16 | 17 | const keys: number[] = []; 18 | for await (const k of map.keys()) { 19 | keys.push(k); 20 | } 21 | expect(keys).toEqual([1, 2, 3]); 22 | 23 | const values: string[] = []; 24 | for await (const v of map.values()) { 25 | values.push(v); 26 | } 27 | expect(values).toEqual(["test", "test2", "test3"]); 28 | 29 | const pairs: Array<[number, string]> = []; 30 | for await (const p of map.keysAndValues()) { 31 | pairs.push(p); 32 | } 33 | expect(pairs).toEqual([ 34 | [1, "test"], 35 | [2, "test2"], 36 | [3, "test3"] 37 | ]); 38 | expect(await map.toArray()).toEqual([ 39 | [1, "test"], 40 | [2, "test2"], 41 | [3, "test3"] 42 | ]); 43 | }); -------------------------------------------------------------------------------- /src/utils/__tests__/hasPrototypeOf_Spec.ts: -------------------------------------------------------------------------------- 1 | import { hasPrototypeOf } from "../hasPrototypeOf"; 2 | 3 | it("Works for classes", () => { 4 | class A { } 5 | class B extends A { } 6 | class C extends B { } 7 | expect(hasPrototypeOf(C, B)).toBeTruthy(); 8 | expect(hasPrototypeOf(C, A)).toBeTruthy(); 9 | expect(hasPrototypeOf(A, B)).toBeFalsy(); 10 | 11 | class D extends Map { } 12 | class E extends D { } 13 | expect(hasPrototypeOf(E, D)).toBeTruthy(); 14 | expect(hasPrototypeOf(E, Map)).toBeTruthy(); 15 | expect(hasPrototypeOf(undefined, Map)).toBeFalsy(); 16 | }); 17 | 18 | it("Works for functions", () => { 19 | function A() { } 20 | function B() { } 21 | B.prototype = Object.create(A.prototype); 22 | B.prototype.constructor = B; 23 | function C() { } 24 | C.prototype = B; 25 | C.prototype = Object.create(B.prototype); 26 | C.prototype.constructor = C; 27 | expect(hasPrototypeOf(C, B)).toBeTruthy(); 28 | expect(hasPrototypeOf(C, A)).toBeTruthy(); 29 | expect(hasPrototypeOf(A, B)).toBeFalsy(); 30 | 31 | function D() { } 32 | D.prototype = Object.create(Map.prototype); 33 | D.prototype.constructor = D; 34 | function E() { } 35 | E.prototype = Object.create(D.prototype); 36 | E.prototype.constructor = E; 37 | expect(hasPrototypeOf(E, D)).toBeTruthy(); 38 | expect(hasPrototypeOf(E, Map)).toBeTruthy(); 39 | expect(hasPrototypeOf(Object, Set)).toBeFalsy(); 40 | }); -------------------------------------------------------------------------------- /src/Decorators/IdentifyProperty.ts: -------------------------------------------------------------------------------- 1 | import { PropertyMetadata, REDIS_PROPERTIES } from "../Metadata/Metadata"; 2 | 3 | /** 4 | * Defines identify property for given redis hash. 5 | * Hash must contain one IdentifyProperty 6 | * 7 | * @export 8 | * @param type Type 9 | * @param [name] Redis property name 10 | * @returns 11 | */ 12 | export function IdentifyProperty(nameOrType?: string | typeof Number | typeof String, type?: typeof Number | typeof String): PropertyDecorator { 13 | return function (target: Object, propertyKey: string): void { 14 | const designType: any = Reflect.getMetadata("design:type", target, propertyKey); 15 | const name = typeof nameOrType === "string" ? nameOrType : propertyKey; 16 | const propertyType = typeof nameOrType === "function" 17 | ? nameOrType 18 | : type 19 | ? type 20 | : designType; 21 | 22 | if (propertyType !== String && propertyType !== Number) { 23 | throw new Error("Identify property must be string or number type"); 24 | } 25 | const properties: PropertyMetadata[] = Reflect.getMetadata(REDIS_PROPERTIES, target.constructor) || []; 26 | properties.push({ 27 | propertyName: propertyKey, 28 | propertyRedisName: name, 29 | isIdentifyColumn: true, 30 | propertyType: propertyType, 31 | isRelation: false 32 | }); 33 | Reflect.defineMetadata(REDIS_PROPERTIES, properties, target.constructor); 34 | }; 35 | } -------------------------------------------------------------------------------- /src/Decorators/__tests__/IdentifyProperty_Spec.ts: -------------------------------------------------------------------------------- 1 | import { REDIS_PROPERTIES } from "../../Metadata/Metadata"; 2 | import { ShouldThrowError } from "../../testutils/ShouldThrowError"; 3 | import { Entity } from "../Entity"; 4 | import { IdentifyProperty } from "../IdentifyProperty"; 5 | 6 | @Entity() 7 | class Test { 8 | @IdentifyProperty(String) 9 | public id: string; 10 | } 11 | 12 | @Entity() 13 | class Test2 { 14 | @IdentifyProperty("somename") 15 | public id: number; 16 | } 17 | 18 | class Invalid1 { 19 | public id: Date; 20 | } 21 | 22 | class Invalid2 { 23 | public id: Object; 24 | } 25 | 26 | it("Throws error if type of property is not number or string", () => { 27 | try { 28 | const test = new Invalid1(); 29 | IdentifyProperty(Date as any)(test, "id"); 30 | throw new ShouldThrowError(); 31 | } catch (e) { 32 | if (e instanceof ShouldThrowError) { throw e; } 33 | } 34 | try { 35 | const test = new Invalid2(); 36 | IdentifyProperty(Object as any)(test, "id"); 37 | throw new ShouldThrowError(); 38 | } catch (e) { 39 | if (e instanceof ShouldThrowError) { throw e; } 40 | } 41 | }); 42 | 43 | it("Defines metadata without custom name", () => { 44 | const metadata = Reflect.getMetadata(REDIS_PROPERTIES, Test); 45 | expect(metadata).toMatchSnapshot(); 46 | }); 47 | 48 | it("Defines metadata with custom name", () => { 49 | const metadata = Reflect.getMetadata(REDIS_PROPERTIES, Test2); 50 | expect(metadata).toMatchSnapshot(); 51 | }); -------------------------------------------------------------------------------- /src/Decorators/__tests__/__snapshots__/RelationProperty_Spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`For set or map of relations 1`] = ` 4 | Array [ 5 | Object { 6 | "isRelation": true, 7 | "propertyName": "test", 8 | "propertyRedisName": "test", 9 | "propertyType": [Function], 10 | "relationOptions": Object { 11 | "cascadeInsert": false, 12 | "cascadeUpdate": false, 13 | }, 14 | "relationTypeFunc": [Function], 15 | }, 16 | Object { 17 | "isRelation": true, 18 | "propertyName": "test2", 19 | "propertyRedisName": "test2", 20 | "propertyType": [Function], 21 | "relationOptions": Object { 22 | "cascadeInsert": false, 23 | "cascadeUpdate": false, 24 | }, 25 | "relationTypeFunc": [Function], 26 | }, 27 | ] 28 | `; 29 | 30 | exports[`With default values 1`] = ` 31 | Array [ 32 | Object { 33 | "isRelation": true, 34 | "propertyName": "test", 35 | "propertyRedisName": "test", 36 | "propertyType": [Function], 37 | "relationOptions": Object { 38 | "cascadeInsert": false, 39 | "cascadeUpdate": false, 40 | }, 41 | "relationTypeFunc": [Function], 42 | }, 43 | ] 44 | `; 45 | 46 | exports[`With relation options 1`] = ` 47 | Array [ 48 | Object { 49 | "isRelation": true, 50 | "propertyName": "test", 51 | "propertyRedisName": "testName", 52 | "propertyType": [Function], 53 | "relationOptions": Object { 54 | "cascadeInsert": false, 55 | "cascadeUpdate": true, 56 | "propertyName": "testName", 57 | }, 58 | "relationTypeFunc": [Function], 59 | }, 60 | ] 61 | `; 62 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { LazyMap } from "./Collections/LazyMap"; 2 | import { LazySet } from "./Collections/LazySet"; 3 | import { RedisLazyMap } from "./Collections/RedisLazyMap"; 4 | import { RedisLazySet } from "./Collections/RedisLazySet"; 5 | import { Connection, ConnectionOptions } from "./Connection/Connection"; 6 | import { RedisManager } from "./Persistence/RedisManager"; 7 | import { EntitySubscriberInterface } from "./Subscriber/EntitySubscriberInterface"; 8 | import { PubSubSubscriberInterface } from "./Subscriber/PubSubSubscriberInterface"; 9 | import { getFromContainer, useContainer } from "./utils/Container"; 10 | 11 | 12 | /** 13 | * Create redis connection 14 | * 15 | * @export 16 | * @param options 17 | * @returns 18 | */ 19 | export async function createRedisConnection(options: ConnectionOptions): Promise { 20 | const conn = getFromContainer(Connection); 21 | await conn.connect(options); 22 | return conn; 23 | } 24 | 25 | /** 26 | * Return redis manager 27 | * 28 | * @export 29 | * @returns 30 | */ 31 | export function getRedisManager(): RedisManager { 32 | const conn = getFromContainer(Connection); 33 | return conn.manager; 34 | } 35 | 36 | export { Connection }; 37 | export { ConnectionOptions }; 38 | export { RedisManager }; 39 | export { LazyMap }; 40 | export { LazySet }; 41 | export { RedisLazyMap }; 42 | export { RedisLazySet }; 43 | export { EntitySubscriberInterface }; 44 | export { PubSubSubscriberInterface }; 45 | export * from "./Metadata/Metadata"; 46 | export * from "./Errors/Errors"; 47 | export * from "./Decorators/Entity"; 48 | export * from "./Decorators/IdentifyProperty"; 49 | export * from "./Decorators/Property"; 50 | export * from "./Decorators/RelationProperty"; 51 | export { useContainer }; -------------------------------------------------------------------------------- /src/Decorators/__tests__/Property_Spec.ts: -------------------------------------------------------------------------------- 1 | import { PropertyMetadata, REDIS_PROPERTIES } from "../../Metadata/Metadata"; 2 | import { Property } from "../Property"; 3 | 4 | // class Internal { } 5 | 6 | class Test { 7 | @Property() 8 | public one: string; 9 | @Property("twoName") 10 | public two: number; 11 | @Property(Date) 12 | public three: Date; 13 | @Property() 14 | public four: Object; 15 | @Property("fiveName") 16 | public five: { id: string }; 17 | @Property() 18 | public six: boolean; 19 | // @Property(Int8Array, "sevenName") 20 | // public seven: Int8Array; 21 | @Property(Set) 22 | public eight: Set; 23 | @Property("nineName", Map) 24 | public nine: Map; 25 | // @Property() 26 | // public ten: Internal; 27 | @Property() 28 | public eleven: string[]; 29 | 30 | public nonRedis1: string; 31 | public nonRedis2: number; 32 | } 33 | 34 | it("Defines metadata", () => { 35 | const metadata: PropertyMetadata[] = Reflect.getMetadata(REDIS_PROPERTIES, Test); 36 | expect(metadata).toHaveLength(9); 37 | const [one, two, three, four, five, six, /* seven, */ eight, nine, /* ten, */eleven] = metadata; 38 | expect(metadata).toMatchSnapshot(); 39 | expect(one.propertyType).toBe(String); 40 | expect(two.propertyType).toBe(Number); 41 | expect(three.propertyType).toBe(Date); 42 | expect(four.propertyType).toBe(Object); 43 | expect(five.propertyType).toBe(Object); 44 | expect(six.propertyType).toBe(Boolean); 45 | // expect(seven.propertyType).toBe(Int8Array); 46 | expect(eight.propertyType).toBe(Set); 47 | expect(nine.propertyType).toBe(Map); 48 | // expect(ten.propertyType).toBe(Internal); 49 | expect(eleven.propertyType).toBe(Array); 50 | }); -------------------------------------------------------------------------------- /src/Decorators/__tests__/__snapshots__/Property_Spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Defines metadata 1`] = ` 4 | Array [ 5 | Object { 6 | "isIdentifyColumn": false, 7 | "isRelation": false, 8 | "propertyName": "one", 9 | "propertyRedisName": "one", 10 | "propertyType": [Function], 11 | }, 12 | Object { 13 | "isIdentifyColumn": false, 14 | "isRelation": false, 15 | "propertyName": "two", 16 | "propertyRedisName": "twoName", 17 | "propertyType": [Function], 18 | }, 19 | Object { 20 | "isIdentifyColumn": false, 21 | "isRelation": false, 22 | "propertyName": "three", 23 | "propertyRedisName": "three", 24 | "propertyType": [Function], 25 | }, 26 | Object { 27 | "isIdentifyColumn": false, 28 | "isRelation": false, 29 | "propertyName": "four", 30 | "propertyRedisName": "four", 31 | "propertyType": [Function], 32 | }, 33 | Object { 34 | "isIdentifyColumn": false, 35 | "isRelation": false, 36 | "propertyName": "five", 37 | "propertyRedisName": "fiveName", 38 | "propertyType": [Function], 39 | }, 40 | Object { 41 | "isIdentifyColumn": false, 42 | "isRelation": false, 43 | "propertyName": "six", 44 | "propertyRedisName": "six", 45 | "propertyType": [Function], 46 | }, 47 | Object { 48 | "isIdentifyColumn": false, 49 | "isRelation": false, 50 | "propertyName": "eight", 51 | "propertyRedisName": "eight", 52 | "propertyType": [Function], 53 | }, 54 | Object { 55 | "isIdentifyColumn": false, 56 | "isRelation": false, 57 | "propertyName": "nine", 58 | "propertyRedisName": "nineName", 59 | "propertyType": [Function], 60 | }, 61 | Object { 62 | "isIdentifyColumn": false, 63 | "isRelation": false, 64 | "propertyName": "eleven", 65 | "propertyRedisName": "eleven", 66 | "propertyType": [Function], 67 | }, 68 | ] 69 | `; 70 | -------------------------------------------------------------------------------- /src/Subscriber/PubSubSubscriberInterface.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * PubSub listener interface 4 | * 5 | * @export 6 | * @interface PubSubSubscriberInterface 7 | */ 8 | export interface PubSubSubscriberInterface { 9 | /** 10 | * New subscribed channel message 11 | * 12 | * @param channel 13 | * @param message 14 | */ 15 | onMessage?(channel: string, message: string): void; 16 | 17 | /** 18 | * New pattern channel message 19 | * 20 | * @param pattern 21 | * @param channel 22 | * @param message 23 | */ 24 | onPMessage?(pattern: string, channel: string, message: string): void; 25 | 26 | /** 27 | * Same as onMessage() but emits a buffer. If there is onMessage listener then emits string 28 | * 29 | * @param channel 30 | * @param message 31 | */ 32 | onMessageBuffer?(channel: string, message: string | Buffer): void; 33 | 34 | /** 35 | * Same as onPMessage() but emits a buffer. If there is onPMessage listener then emits string 36 | * 37 | * @param pattern 38 | * @param channel 39 | * @param message 40 | */ 41 | onPMessageBuffer?(pattern: string, channel: string, message: string | Buffer): void; 42 | 43 | /** 44 | * On new subscription 45 | * 46 | * @param channel 47 | * @param count 48 | */ 49 | onSubscribe?(channel: string, count: number): void; 50 | 51 | /** 52 | * On new pattern subscription 53 | * 54 | * @param pattern 55 | * @param count 56 | */ 57 | onPSubscribe?(pattern: string, count: number): void; 58 | 59 | /** 60 | * On channel unsubscribe 61 | * 62 | * @param channel 63 | * @param count 64 | */ 65 | onUnsubscribe?(channel: string, count: number): void; 66 | 67 | /** 68 | * On pattern unsbuscribe 69 | * 70 | * @param pattern 71 | * @param count 72 | */ 73 | onPUnsubscribe?(pattern: string, count: number): void; 74 | } -------------------------------------------------------------------------------- /src/Decorators/Property.ts: -------------------------------------------------------------------------------- 1 | import { LazyMap } from "../Collections/LazyMap"; 2 | import { LazySet } from "../Collections/LazySet"; 3 | import { PropertyMetadata, REDIS_PROPERTIES } from "../Metadata/Metadata"; 4 | 5 | export type ValidType = 6 | typeof Number | 7 | typeof String | 8 | typeof Boolean | 9 | typeof Object | 10 | typeof Array | 11 | typeof Date | 12 | typeof Map | 13 | typeof LazyMap | 14 | typeof LazySet | 15 | typeof Set; 16 | 17 | 18 | /** 19 | * Defines redis hash property 20 | * 21 | * @export 22 | * @param [type] 23 | * @returns 24 | */ 25 | export function Property(type?: ValidType): PropertyDecorator; 26 | /** 27 | * Defines redis hash property 28 | * 29 | * @export 30 | * @param name Redis name for property 31 | * @param [type] 32 | * @returns 33 | */ 34 | export function Property(name: string, type?: ValidType): PropertyDecorator; 35 | 36 | /** 37 | * Defines redis hash property 38 | * 39 | * @export 40 | * @param [name] Property name to store in redis 41 | * @returns 42 | */ 43 | export function Property(nameOrType?: string | ValidType, type?: ValidType): PropertyDecorator { 44 | return function (target: Object, propertyKey: string): void { 45 | const designType: any = Reflect.getMetadata("design:type", target, propertyKey); 46 | const name = typeof nameOrType === "string" ? nameOrType : propertyKey; 47 | const propertyType = typeof nameOrType === "function" 48 | ? nameOrType 49 | : type 50 | ? type 51 | : designType; 52 | 53 | const properties: PropertyMetadata[] = Reflect.getMetadata(REDIS_PROPERTIES, target.constructor) || []; 54 | properties.push({ 55 | propertyName: propertyKey, 56 | propertyRedisName: name, 57 | isIdentifyColumn: false, 58 | propertyType: propertyType, 59 | isRelation: false, 60 | }); 61 | Reflect.defineMetadata(REDIS_PROPERTIES, properties, target.constructor); 62 | }; 63 | } -------------------------------------------------------------------------------- /src/Collections/LazySet.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Lazy set 4 | * 5 | * @export 6 | * @class LazySet 7 | * @template T 8 | */ 9 | export class LazySet { 10 | /** 11 | * Set instance 12 | * 13 | * @private 14 | */ 15 | private set: Set; 16 | 17 | /** 18 | * Creates an instance of LazySet. 19 | * @param [values] 20 | */ 21 | public constructor(values?: T[]) { 22 | this.set = new Set(values); 23 | } 24 | 25 | /** 26 | * Add value to the set 27 | * 28 | * @param value 29 | * @returns 30 | */ 31 | public async add(value: T): Promise { 32 | this.set.add(value); 33 | } 34 | 35 | /** 36 | * Delete value from the set 37 | * 38 | * @param value 39 | * @param [deleteEntity=false] Also delete entity if it's entity set 40 | * @returns 41 | */ 42 | public async delete(value: T, deleteEntity: boolean = false): Promise { 43 | return this.set.delete(value); 44 | } 45 | 46 | /** 47 | * Check if has value in the set 48 | * 49 | * @param value 50 | * @returns 51 | */ 52 | public async has(value: T): Promise { 53 | return this.set.has(value); 54 | } 55 | 56 | /** 57 | * Get set size 58 | * 59 | * @returns 60 | */ 61 | public async size(): Promise { 62 | return this.set.size; 63 | } 64 | 65 | /** 66 | * Clear set 67 | * 68 | * @param [deleteEntities=false] Also delete all entities 69 | * @returns 70 | */ 71 | public async clear(deleteEntities: boolean = false): Promise { 72 | this.set.clear(); 73 | } 74 | 75 | /** 76 | * Get all values in array 77 | * 78 | * @returns 79 | */ 80 | public async toArray(): Promise { 81 | return [...this.set.values()]; 82 | } 83 | 84 | /** 85 | * Iterate over values 86 | * @param scanCount Redis SCAN's COUNT option 87 | */ 88 | public async * values(scanCount?: number) { 89 | for (const val of this.set.values()) { 90 | yield val; 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "orm-redis", 3 | "version": "0.1.10", 4 | "description": "ORM Like Redis mapper for Typescript", 5 | "main": "./compiled/index.js", 6 | "types": "./compiled/index.d.ts", 7 | "scripts": { 8 | "clean": "node ./helpers/clean.js", 9 | "ts": "tsc -p tsconfig-build.json", 10 | "ts:watch": "tsc -w -p tsconfig-build.json", 11 | "test": "cross-env REDIS_HOST=localhost REDIS_PORT=6379 jest", 12 | "lint": "tslint 'src/**/*'", 13 | "build": "run-s clean ts lint", 14 | "test:coverage": "cross-env REDIS_HOST=localhost REDIS_PORT=6379 jest --coverage --no-cache", 15 | "prepublishOnly": "npm run build" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/asvetliakov/orm-redis" 20 | }, 21 | "author": "Alexey Svetliakov ", 22 | "license": "ISC", 23 | "devDependencies": { 24 | "@types/jest": "^20.0.4", 25 | "cross-env": "^5.0.1", 26 | "del": "^3.0.0", 27 | "jest": "^20.0.4", 28 | "jest-environment-node-debug": "^2.0.0", 29 | "npm-run-all": "^4.0.2", 30 | "reflect-metadata": "^0.1.10", 31 | "ts-jest": "^20.0.7", 32 | "tslint": "^5.5.0", 33 | "typescript": "^2.4.1" 34 | }, 35 | "dependencies": { 36 | "@types/redis": "*", 37 | "redis": "^2.7.1", 38 | "sb-promisify": "^2.0.2" 39 | }, 40 | "jest": { 41 | "globals": { 42 | "ts-jest": { 43 | "tsConfigFile": "tsconfig.json" 44 | } 45 | }, 46 | "testEnvironment": "node", 47 | "resetMocks": true, 48 | "transform": { 49 | "\\.(ts|tsx)": "/node_modules/ts-jest/preprocessor.js" 50 | }, 51 | "testRegex": "(/__tests__/.*|.*_[Ff]?[Ss]pec)\\.(ts|tsx|js)$", 52 | "moduleFileExtensions": [ 53 | "js", 54 | "jsx", 55 | "ts", 56 | "tsx" 57 | ], 58 | "collectCoverageFrom": [ 59 | "src/**/*.ts", 60 | "!src/**/*.d.ts", 61 | "!src/index.ts", 62 | "!src/testutils/**/*" 63 | ], 64 | "setupTestFrameworkScriptFile": "/helpers/test-helper.js", 65 | "coverageReporters": [ 66 | "json", 67 | "lcov", 68 | "text", 69 | "cobertura" 70 | ], 71 | "roots": [ 72 | "/src" 73 | ], 74 | "mapCoverage": true 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Decorators/__tests__/RelationProperty_Spec.ts: -------------------------------------------------------------------------------- 1 | import { PropertyMetadata, REDIS_PROPERTIES, RelationPropertyMetadata } from "../../Metadata/Metadata"; 2 | import { Entity } from "../Entity"; 3 | import { RelationProperty } from "../RelationProperty"; 4 | @Entity() 5 | class Rel { } 6 | 7 | it("With default values", () => { 8 | @Entity() 9 | class C { 10 | @RelationProperty(() => Rel) 11 | public test: Rel; 12 | } 13 | const metadata: PropertyMetadata[] = Reflect.getMetadata(REDIS_PROPERTIES, C); 14 | expect(metadata).toMatchSnapshot(); 15 | const relMetadata: RelationPropertyMetadata = metadata[0] as RelationPropertyMetadata; 16 | expect(relMetadata.relationTypeFunc()).toBe(Rel); 17 | expect(relMetadata.propertyType).toBe(Rel); 18 | }); 19 | 20 | it("With relation options", () => { 21 | @Entity() 22 | class C { 23 | @RelationProperty(type => Rel, { cascadeUpdate: true, propertyName: "testName" }) 24 | public test: Rel; 25 | } 26 | const metadata: PropertyMetadata[] = Reflect.getMetadata(REDIS_PROPERTIES, C); 27 | expect(metadata).toMatchSnapshot(); 28 | }); 29 | 30 | it("With explicitly specified property type", () => { 31 | @Entity() 32 | class C { 33 | @RelationProperty(() => [Rel, Rel]) 34 | public test: Rel; 35 | } 36 | const metadata: PropertyMetadata[] = Reflect.getMetadata(REDIS_PROPERTIES, C); 37 | const relMetadata: RelationPropertyMetadata = metadata[0] as RelationPropertyMetadata; 38 | expect(relMetadata.relationTypeFunc()).toEqual([Rel, Rel]); 39 | expect(relMetadata.propertyType).toBe(Rel); 40 | }); 41 | 42 | it("For set or map of relations", () => { 43 | @Entity() 44 | class C { 45 | @RelationProperty(() => [Rel, Set]) 46 | public test: Set; 47 | 48 | @RelationProperty(() => [Rel, Map]) 49 | public test2: Map; 50 | } 51 | const metadata: PropertyMetadata[] = Reflect.getMetadata(REDIS_PROPERTIES, C); 52 | expect(metadata).toMatchSnapshot(); 53 | let relMetadata: RelationPropertyMetadata = metadata[0] as RelationPropertyMetadata; 54 | expect(relMetadata.relationTypeFunc()).toEqual([Rel, Set]); 55 | expect(relMetadata.propertyType).toBe(Set); 56 | relMetadata = metadata[1] as RelationPropertyMetadata; 57 | expect(relMetadata.relationTypeFunc(0)).toEqual([Rel, Map]); 58 | expect(relMetadata.propertyType).toBe(Map); 59 | }); -------------------------------------------------------------------------------- /src/Decorators/RelationProperty.ts: -------------------------------------------------------------------------------- 1 | import { MetadataError } from "../Errors/Errors"; 2 | import { PropertyMetadata, REDIS_PROPERTIES, RelationOptions, RelationTypeFunc } from "../Metadata/Metadata"; 3 | 4 | /** 5 | * default options 6 | */ 7 | const defaultOptions: RelationOptions = { 8 | cascadeInsert: false, 9 | cascadeUpdate: false 10 | }; 11 | 12 | /** 13 | * Defines single or multiple relation property 14 | * 15 | * @export 16 | * @param type Type function which must return type of relation / property type 17 | * @param propertyType Property type. For single relation must be 18 | */ 19 | export function RelationProperty(type: RelationTypeFunc): PropertyDecorator; 20 | /** 21 | * Defines single or multiple relation property 22 | * 23 | * @export 24 | * @param type Type function which must return type of relation / property type 25 | * @param [options] Options 26 | */ 27 | export function RelationProperty(type: RelationTypeFunc, options?: RelationOptions): PropertyDecorator; 28 | 29 | /** 30 | * Defines single or multiple relation property 31 | * 32 | * @export 33 | * @param type 34 | * @param [options] 35 | * @returns 36 | */ 37 | export function RelationProperty(type: RelationTypeFunc, options?: RelationOptions): PropertyDecorator { 38 | return function (target: Object, propertyKey: string): void { 39 | const designType = Reflect.getMetadata("design:type", target, propertyKey); 40 | 41 | const redisPropertyName = options && options.propertyName ? options.propertyName : propertyKey; 42 | 43 | const relationTypes = type(); 44 | 45 | let propertyType: Function; 46 | if (Array.isArray(relationTypes)) { 47 | [, propertyType] = relationTypes; 48 | } else { 49 | if (designType === Object) { 50 | throw new MetadataError(target.constructor, `Relation's ${propertyKey} property type is detected as simple Object. This is error. Specify property type explicitly`); 51 | } 52 | propertyType = designType; 53 | } 54 | 55 | const finalOptions = { 56 | ...defaultOptions, 57 | ...options 58 | }; 59 | 60 | const properties: PropertyMetadata[] = Reflect.getMetadata(REDIS_PROPERTIES, target.constructor) || []; 61 | properties.push({ 62 | isRelation: true, 63 | propertyName: propertyKey, 64 | propertyRedisName: redisPropertyName, 65 | propertyType: propertyType, 66 | relationTypeFunc: type, 67 | relationOptions: finalOptions 68 | }); 69 | Reflect.defineMetadata(REDIS_PROPERTIES, properties, target.constructor); 70 | }; 71 | } -------------------------------------------------------------------------------- /src/Collections/LazyMap.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Lazy map 3 | * 4 | * @export 5 | * @class LazyMap 6 | * @template K 7 | * @template V 8 | */ 9 | export class LazyMap { 10 | 11 | /** 12 | * Map backend instance for non initialized lazy map 13 | * 14 | * @private 15 | */ 16 | private map: Map; 17 | 18 | /** 19 | * Creates an instance of LazyMap. 20 | * @param [entries] 21 | */ 22 | public constructor(entries?: Array<[K, V]>) { 23 | this.map = new Map(entries); 24 | } 25 | 26 | /** 27 | * Clear map 28 | * 29 | * @param [deleteEntities=false] Also delete entties 30 | * @returns 31 | */ 32 | public async clear(deleteEntities: boolean = false): Promise { 33 | this.map.clear(); 34 | } 35 | 36 | /** 37 | * Delete value by key 38 | * 39 | * @param key 40 | * @param [deleteEntity=false] Also delete corresponding entity 41 | * @returns 42 | */ 43 | public async delete(key: K, deleteEntity: boolean = false): Promise { 44 | return this.map.delete(key); 45 | } 46 | 47 | /** 48 | * Get value by key 49 | * 50 | * @param key 51 | * @returns 52 | */ 53 | public async get(key: K): Promise { 54 | return this.map.get(key); 55 | } 56 | 57 | /** 58 | * Check if has value by key 59 | * 60 | * @param key 61 | * @returns 62 | */ 63 | public async has(key: K): Promise { 64 | return this.map.has(key); 65 | } 66 | 67 | /** 68 | * Set new value 69 | * 70 | * @param key 71 | * @param value 72 | * @returns 73 | */ 74 | public async set(key: K, value: V): Promise { 75 | this.map.set(key, value); 76 | } 77 | 78 | /** 79 | * Map size 80 | * 81 | * @returns 82 | */ 83 | public async size(): Promise { 84 | return this.map.size; 85 | } 86 | 87 | /** 88 | * Convert to array 89 | * 90 | * @returns 91 | */ 92 | public async toArray(): Promise> { 93 | return [...this.map]; 94 | } 95 | 96 | /** 97 | * Keys iterator 98 | * @param scanCount Redis SCAN's COUNT option 99 | * 100 | * @returns 101 | */ 102 | public async * keys(scanCount?: number): AsyncIterableIterator { 103 | for (const k of this.map.keys()) { 104 | yield k; 105 | } 106 | } 107 | 108 | /** 109 | * Values iterator 110 | * @param scanCount Redis SCAN's COUNT option 111 | * 112 | * @returns 113 | */ 114 | public async * values(scanCount?: number): AsyncIterableIterator { 115 | for (const v of this.map.values()) { 116 | yield v; 117 | } 118 | } 119 | 120 | /** 121 | * Map iterator 122 | * 123 | * @returns 124 | */ 125 | public async * keysAndValues(scanCount?: number): AsyncIterableIterator<[K, V]> { 126 | for (const val of this.map) { 127 | yield val; 128 | } 129 | } 130 | } -------------------------------------------------------------------------------- /src/testutils/redis.ts: -------------------------------------------------------------------------------- 1 | import { Connection, ConnectionOptions } from "../Connection/Connection"; 2 | import { createRedisConnection as createRedisConnectionOriginal } from "../index"; 3 | import * as redis from "../utils/PromisedRedis"; 4 | 5 | 6 | /** 7 | * Get available database from pool of databases (presented as SET in redis) 8 | * 9 | * @returns 10 | */ 11 | export async function getAvailableDatabase(): Promise { 12 | const client = createRawRedisClient(); 13 | let dbNumber: string | null = await client.spopAsync("available_db"); 14 | // Set doesn't exist 15 | if (!dbNumber) { 16 | // Create set of db connetions 17 | await client.saddAsync("available_db", ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15"]); 18 | // Obtain again 19 | dbNumber = await client.spopAsync("available_db"); 20 | } 21 | if (!dbNumber) { 22 | throw new Error("Unable to obtain database number"); 23 | } 24 | await client.quitAsync(); 25 | return dbNumber; 26 | } 27 | 28 | /** 29 | * Release available database and return it to the pool 30 | * 31 | * @export 32 | * @param dbNumber 33 | * @returns 34 | */ 35 | export async function releaseDatabase(dbNumber: string): Promise { 36 | const client = redis.createClient(parseInt(process.env.REDIS_PORT!) || 6379, process.env.REDIS_HOST); 37 | await client.saddAsync("available_db", dbNumber); 38 | await client.quitAsync(); 39 | } 40 | 41 | /** 42 | * Create redis client 43 | * 44 | * @export 45 | * @returns 46 | */ 47 | export function createRawRedisClient(): redis.RedisClient { 48 | const client = redis.createClient(parseInt(process.env.REDIS_PORT!) || 6379, process.env.REDIS_HOST); 49 | return client; 50 | } 51 | 52 | 53 | // Just in case if the global state will be shared between tests 54 | const connectionToDbNumber = new WeakMap(); 55 | 56 | /** 57 | * Create redis connection 58 | * 59 | * @param [options={}] 60 | * @export 61 | * @returns 62 | */ 63 | export async function createRedisConnection(options: ConnectionOptions = {}): Promise { 64 | const dbNumber = await getAvailableDatabase(); 65 | let connection: Connection; 66 | try { 67 | connection = await createRedisConnectionOriginal({ 68 | db: dbNumber, 69 | host: process.env.REDIS_HOST, 70 | port: parseInt(process.env.REDIS_PORT!) || 6379, 71 | ...options 72 | }); 73 | connectionToDbNumber.set(connection, dbNumber); 74 | } catch (e) { 75 | await releaseDatabase(dbNumber); 76 | throw e; 77 | } 78 | return connection; 79 | } 80 | 81 | /** 82 | * Clean & release redis connection 83 | * 84 | * @export 85 | * @param connection 86 | * @returns 87 | */ 88 | export async function cleanRedisConnection(connection: Connection): Promise { 89 | if (connection.isConnected) { 90 | await connection.flushdb(); 91 | } 92 | const dbNumber = connectionToDbNumber.get(connection); 93 | if (dbNumber) { 94 | await releaseDatabase(dbNumber); 95 | } 96 | if (connection.isConnected) { 97 | await connection.disconnect(); 98 | } 99 | } 100 | 101 | /** 102 | * Return database number used for connection 103 | * 104 | * @export 105 | * @param connection 106 | * @returns 107 | */ 108 | export function getDatabaseNumberForConnection(connection: Connection): string | undefined { 109 | return connectionToDbNumber.get(connection); 110 | } -------------------------------------------------------------------------------- /src/testutils/RedisTestMonitor.ts: -------------------------------------------------------------------------------- 1 | import { RedisClient } from "redis"; 2 | import { Connection } from "../Connection/Connection"; 3 | import { createRawRedisClient, getDatabaseNumberForConnection } from "./redis"; 4 | 5 | export class RedisTestMonitor { 6 | // Array of calls -> results without any timestamps 7 | private _calls: string[][][] = []; 8 | 9 | /** 10 | * Redis client 11 | * 12 | * @private 13 | */ 14 | private client: RedisClient; 15 | 16 | /** 17 | * Filter calls for specific redis db (each test connection connects to different db) 18 | * 19 | * @private 20 | */ 21 | private filterDbNumber?: string; 22 | 23 | /** 24 | * All monitor calls in format [request, response] 25 | * 26 | * @readonly 27 | */ 28 | public get calls(): string[][][] { 29 | return this._calls; 30 | } 31 | 32 | /** 33 | * Request only calls 34 | * 35 | * @readonly 36 | */ 37 | public get requests(): string[][] { 38 | return this._calls.map(([request, result]) => request); 39 | } 40 | 41 | /** 42 | * Responses only 43 | * 44 | * @readonly 45 | */ 46 | public get responses(): string[][] { 47 | return this._calls.map(([request, result]) => result); 48 | } 49 | 50 | 51 | /** 52 | * Release monitor 53 | * 54 | * @returns 55 | */ 56 | public async release(): Promise { 57 | await this.client.quitAsync(); 58 | } 59 | 60 | /** 61 | * Reset monitor calls 62 | * @param timeToWaitMs Wait some time before clearing 63 | * 64 | */ 65 | public async clearMonitorCalls(timeToWaitMs?: number): Promise { 66 | if (timeToWaitMs) { 67 | await this.wait(timeToWaitMs); 68 | } 69 | this._calls = []; 70 | } 71 | 72 | /** 73 | * Wait some time 74 | * 75 | * @param timeToWaitMs 76 | * @returns 77 | */ 78 | public async wait(timeToWaitMs: number): Promise { 79 | await new Promise(resolve => setTimeout(resolve, timeToWaitMs)); 80 | } 81 | 82 | /** 83 | * Logger 84 | * 85 | * @protected 86 | * @param time 87 | * @param commandArgs 88 | * @param reply 89 | * @returns 90 | */ 91 | protected monitor(time: number, commandArgs: string[], reply: string): void { 92 | const repliesArgs = reply.split(" "); // 1500637727.256744 [12 172.20.0.1:48986] \"multi\" 93 | const justReply = repliesArgs.splice(3).map(r => r.slice(1, -1)); 94 | const dbNumber = repliesArgs[1].slice(1); // [12 172.20.0.1:48986] 95 | if (this.filterDbNumber) { 96 | if (!dbNumber) { 97 | return; 98 | } 99 | if (dbNumber !== this.filterDbNumber) { 100 | return; 101 | } 102 | } 103 | this._calls.push([commandArgs, justReply as any]); 104 | } 105 | 106 | /** 107 | * Create monitor 108 | * 109 | * @static 110 | * @param [connection] Monitor only for requests/replies for given connection db 111 | * @returns 112 | */ 113 | public static async create(connection?: Connection): Promise { 114 | const instance = new RedisTestMonitor(); 115 | if (connection) { 116 | instance.filterDbNumber = getDatabaseNumberForConnection(connection); 117 | } 118 | instance.client = await createRawRedisClient(); 119 | instance.client.on("monitor", instance.monitor.bind(instance)); 120 | await instance.client.monitorAsync(); 121 | return instance; 122 | } 123 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "align": [ 4 | true, 5 | "statements", 6 | "members", 7 | "elements" 8 | ], 9 | "class-name": true, 10 | "comment-format": [ 11 | false, 12 | "check-space" 13 | ], 14 | "curly": true, 15 | "indent": [ 16 | true, 17 | "spaces" 18 | ], 19 | "jsdoc-format": true, 20 | "member-access": [ 21 | true, 22 | "check-accessor", 23 | "check-constructor" 24 | ], 25 | "member-ordering": [ 26 | true, 27 | { 28 | "order": "instance-sandwich" 29 | } 30 | ], 31 | "new-parens": true, 32 | "no-angle-bracket-type-assertion": true, 33 | "no-arg": true, 34 | "no-conditional-assignment": true, 35 | "no-construct": true, 36 | "no-parameter-properties": true, 37 | "no-debugger": true, 38 | "no-duplicate-variable": true, 39 | "no-duplicate-super": true, 40 | "no-eval": true, 41 | "no-internal-module": true, 42 | "no-namespace": [ 43 | true, 44 | "allow-declarations" 45 | ], 46 | "no-trailing-whitespace": false, 47 | // "no-use-before-declare": true, 48 | "no-var-keyword": true, 49 | "no-var-requires": true, 50 | "no-invalid-template-strings": true, 51 | "no-invalid-this": [ 52 | true, 53 | "check-function-in-method" 54 | ], 55 | "import-spacing": true, 56 | "object-literal-sort-keys": false, 57 | "one-line": [ 58 | true, 59 | "check-catch", 60 | "check-else", 61 | "check-finally", 62 | "check-open-brace", 63 | "check-whitespace" 64 | ], 65 | "one-variable-per-declaration": [ 66 | true, 67 | "ignore-for-loop" 68 | ], 69 | "quotemark": [ 70 | true, 71 | "double", 72 | "jsx-double", 73 | "avoid-escape" 74 | ], 75 | "semicolon": [ 76 | true, 77 | "always" 78 | ], 79 | "triple-equals": [ 80 | true, 81 | "allow-null-check" 82 | ], 83 | "typedef": [ 84 | true, 85 | "parameter", 86 | "property-declaration" 87 | ], 88 | "typedef-whitespace": [ 89 | true, 90 | { 91 | "call-signature": "nospace", 92 | "index-signature": "nospace", 93 | "parameter": "nospace", 94 | "property-declaration": "nospace", 95 | "variable-declaration": "nospace" 96 | }, 97 | { 98 | "call-signature": "space", 99 | "index-signature": "space", 100 | "parameter": "space", 101 | "property-declaration": "space", 102 | "variable-declaration": "space" 103 | } 104 | ], 105 | "variable-name": [ 106 | true, 107 | "check-format", 108 | "allow-pascal-case", 109 | "ban-keywords", 110 | "allow-leading-underscore" 111 | ], 112 | "whitespace": [ 113 | true, 114 | "check-branch", 115 | "check-decl", 116 | "check-operator", 117 | "check-module", 118 | "check-separator", 119 | "check-type" 120 | ], 121 | // Tslint 4.0 122 | "adjacent-overload-signatures": true, 123 | "array-type": [ 124 | true, 125 | "array-simple" 126 | ], 127 | "no-unsafe-finally": true, 128 | "no-unused-expression": true, 129 | "object-literal-key-quotes": [ 130 | true, 131 | "as-needed" 132 | ], 133 | "prefer-for-of": true, 134 | "prefer-const": true, 135 | // "strict-boolean-expressions": true, 136 | "prefer-method-signature": true, 137 | "no-magic-numbers": false, 138 | // "promise-function-async": true, 139 | // "no-inferred-empty-object-type": true, 140 | "no-string-throw": true, 141 | // "no-empty-interface": true, 142 | "no-null-keyword": false, 143 | "interface-over-type-literal": true, 144 | // "no-void-expression": true, 145 | "ordered-imports": [ 146 | true 147 | ], 148 | // Tslint 4.3 149 | "typeof-compare": true 150 | } 151 | } -------------------------------------------------------------------------------- /src/testutils/TestDecorators.ts: -------------------------------------------------------------------------------- 1 | import { getEntityId, getEntityName, isRedisEntity, REDIS_COLLECTION_VALUE, REDIS_VALUE } from "../Metadata/Metadata"; 2 | 3 | function prepareValue(value: any, target: any, propertyKey: string): string | undefined { 4 | if (isRedisEntity(value)) { 5 | const hashId = getEntityId(value); 6 | const hashName = getEntityName(value); 7 | return `e:${hashName}:${hashId}`; 8 | } else { 9 | const hashId = getEntityId(target); 10 | const hashName = getEntityName(target); 11 | if (value instanceof Map) { 12 | return `m:e:${hashName}:${hashId}:${propertyKey}`; 13 | } else if (value instanceof Set) { 14 | return `a:e:${hashName}:${hashId}:${propertyKey}`; 15 | } else if (typeof value === "number") { 16 | return `i:${value}`; 17 | } else if (typeof value === "string") { 18 | return `s:${value}`; 19 | } else if (typeof value === "boolean") { 20 | return value ? `b:1` : `b:0`; 21 | } else if (typeof value === "object") { 22 | if (value === null) { 23 | return "null"; 24 | } else if (value instanceof Date) { 25 | return `d:${value.getTime()}`; 26 | } else { 27 | return `j:${JSON.stringify(value)}`; 28 | } 29 | } else { 30 | return `j:${JSON.stringify(value)}`; 31 | } 32 | } 33 | } 34 | 35 | /** 36 | * Decorator to emulate REDIS_VALUE metadata which will be available after entity fetching/saving 37 | * 38 | * @export 39 | * @param value 40 | * @returns 41 | */ 42 | export function TestRedisInitialValue(value?: any): PropertyDecorator { 43 | return function (target: object, propertyKey: string ): PropertyDescriptor | undefined { 44 | // Set explicitly value 45 | if (typeof value !== "undefined") { 46 | const preparedValue = prepareValue(value, target, propertyKey); 47 | Reflect.defineMetadata(REDIS_VALUE, preparedValue, target, propertyKey); 48 | } else { 49 | let val: any; 50 | const descriptor: PropertyDescriptor = { 51 | set: function (newValue: any) { 52 | const metadata = Reflect.getMetadata(REDIS_VALUE, target, propertyKey); 53 | // first setting 54 | if (typeof metadata === "undefined") { 55 | Reflect.defineMetadata(REDIS_VALUE, prepareValue(newValue, target, propertyKey), target, propertyKey); 56 | } 57 | val = newValue; 58 | }, 59 | get: function () { 60 | return val; 61 | } 62 | }; 63 | return descriptor; 64 | } 65 | }; 66 | } 67 | 68 | function getPreparedCollection(coll: Map | Set, target: any, propertyKey: string): string[] | { [key: string]: string } { 69 | if (coll instanceof Map) { 70 | const obj: { [key: string]: string } = {}; 71 | for (const [key, val] of coll) { 72 | const prepkey = prepareValue(key, target, propertyKey); 73 | const prepVal = prepareValue(val, target, propertyKey); 74 | if (typeof prepkey !== "undefined" && typeof prepVal !== "undefined") { 75 | obj[prepkey] = prepVal; 76 | } 77 | } 78 | return obj; 79 | } else { 80 | return [...coll.values()].map(val => prepareValue(val, target, propertyKey)).filter(val => typeof val !== "undefined") as string[]; 81 | } 82 | } 83 | 84 | export function TestRedisInitialCollectionValue(value?: Map | Set): PropertyDecorator { 85 | return function (target: object, propertyKey: string): PropertyDescriptor | undefined { 86 | if (value) { 87 | Reflect.defineMetadata(REDIS_VALUE, prepareValue(value, target, propertyKey), target, propertyKey); 88 | Reflect.defineMetadata(REDIS_COLLECTION_VALUE, getPreparedCollection(value, target, propertyKey), target, propertyKey); 89 | } else { 90 | let val: any; 91 | const descriptor: PropertyDescriptor = { 92 | set: function (newValue: any) { 93 | const metadata = Reflect.getMetadata(REDIS_VALUE, target, propertyKey); 94 | // first setting 95 | if (typeof metadata === "undefined") { 96 | Reflect.defineMetadata(REDIS_VALUE, prepareValue(newValue, target, propertyKey), target, propertyKey); 97 | if (newValue instanceof Map || newValue instanceof Set) { 98 | Reflect.defineMetadata(REDIS_COLLECTION_VALUE, getPreparedCollection(newValue, target, propertyKey), target, propertyKey); 99 | } 100 | } 101 | val = newValue; 102 | }, 103 | get: function () { 104 | return val; 105 | } 106 | }; 107 | return descriptor; 108 | } 109 | }; 110 | } -------------------------------------------------------------------------------- /src/Collections/RedisLazySet.ts: -------------------------------------------------------------------------------- 1 | import { getEntityFullId } from "../Metadata/Metadata"; 2 | import { EntityType, RedisManager } from "../Persistence/RedisManager"; 3 | import { LazySet } from "./LazySet"; 4 | 5 | /** 6 | * Redis-backed lazy set 7 | * 8 | * @export 9 | * @class RedisLazySet 10 | * @template T 11 | */ 12 | export class RedisLazySet extends LazySet { 13 | /** 14 | * Manager instance 15 | * 16 | * @private 17 | */ 18 | private manager: RedisManager; 19 | /** 20 | * Full set id 21 | * 22 | * @private 23 | */ 24 | private setId: string; 25 | 26 | /** 27 | * Entity class 28 | * 29 | * @private 30 | */ 31 | private entityClass?: Function; 32 | 33 | /** 34 | * Cascade insert related entities 35 | * 36 | * @private 37 | */ 38 | private cascadeInsert: boolean; 39 | 40 | /** 41 | * Creates an instance of RedisLazySet. 42 | * @param setId Full set id in redis 43 | * @param manager Manager instance 44 | * @param [entityClass] If passed then set will be treated as entity set and will return entities 45 | */ 46 | public constructor(setId: string, manager: RedisManager, entityClass?: EntityType, cascadeInsert: boolean = false) { 47 | super(); 48 | this.manager = manager; 49 | this.setId = setId; 50 | this.entityClass = entityClass; 51 | this.cascadeInsert = cascadeInsert; 52 | } 53 | 54 | /** 55 | * Add value or entity to the set 56 | * 57 | * @param value 58 | * @returns 59 | */ 60 | public async add(value: T): Promise { 61 | if (this.entityClass) { 62 | // Entity 63 | if (this.cascadeInsert) { 64 | await this.manager.save(value as any); 65 | } 66 | await this.manager.connection.client.saddAsync(this.setId, getEntityFullId(value)!); 67 | } else { 68 | const serializedVal = this.manager.serializeSimpleValue(value); 69 | if (serializedVal) { 70 | await this.manager.connection.client.saddAsync(this.setId, serializedVal); 71 | } 72 | } 73 | } 74 | 75 | /** 76 | * Remove value or entity from the set. 77 | * 78 | * @param value 79 | * @param [deleteEntity=false] 80 | * @returns 81 | */ 82 | public async delete(value: T, deleteEntity: boolean = false): Promise { 83 | let res: number; 84 | if (this.entityClass) { 85 | // Entity 86 | if (deleteEntity) { 87 | await this.manager.remove(value as any); 88 | } 89 | res = await this.manager.connection.client.sremAsync(this.setId, getEntityFullId(value)!); 90 | } else { 91 | const serializedVal = this.manager.serializeSimpleValue(value); 92 | if (serializedVal) { 93 | res = await this.manager.connection.client.sremAsync(this.setId, serializedVal); 94 | } else { 95 | res = 0; 96 | } 97 | } 98 | return res > 0 ? true : false; 99 | } 100 | 101 | /** 102 | * Determine if value or entity exists in the set 103 | * 104 | * @param value 105 | * @returns 106 | */ 107 | public async has(value: T): Promise { 108 | const id = this.entityClass ? getEntityFullId(value)! : this.manager.serializeSimpleValue(value); 109 | if (typeof id === "undefined") { 110 | return false; 111 | } 112 | const res = await this.manager.connection.client.sismemberAsync(this.setId, id); 113 | return !!res; 114 | } 115 | 116 | /** 117 | * Get size of set 118 | * 119 | * @returns 120 | */ 121 | public async size(): Promise { 122 | return await this.manager.connection.client.scardAsync(this.setId); 123 | } 124 | 125 | /** 126 | * Clear set 127 | * 128 | * @param [deleteEntities=false] Also delete all entities 129 | * @returns 130 | */ 131 | public async clear(deleteEntities: boolean = false): Promise { 132 | if (this.entityClass && deleteEntities) { 133 | const setVals = await this.manager.connection.client.smembersAsync(this.setId); 134 | await this.manager.removeById(this.entityClass, setVals); 135 | } 136 | await this.manager.connection.client.delAsync(this.setId); 137 | } 138 | 139 | /** 140 | * Convert set to array 141 | * 142 | * @returns 143 | */ 144 | public async toArray(): Promise { 145 | const results: T[] = []; 146 | for await (const v of this.values()) { 147 | results.push(v); 148 | } 149 | return results; 150 | } 151 | 152 | /** 153 | * Iterate over values 154 | * @param scanCount Redis SCAN's COUNT option 155 | */ 156 | public async * values(scanCount?: number): AsyncIterableIterator { 157 | const scanOption = scanCount ? ["COUNT", scanCount] : []; 158 | let [cursor, results]: [string, any[]] = await this.manager.connection.client.sscanAsync(this.setId, "0", ...scanOption); 159 | // load entities 160 | if (this.entityClass && results.length > 0) { 161 | results = await this.manager.load(this.entityClass, results) as any; 162 | } else { 163 | results = results.map(res => this.manager.unserializeSimpleValue(res)); 164 | } 165 | for (const res of results) { 166 | yield res; 167 | } 168 | while (cursor !== "0") { 169 | [cursor, results] = await this.manager.connection.client.sscanAsync(this.setId, cursor, ...scanOption); 170 | // load entities 171 | if (this.entityClass && results.length > 0) { 172 | results = await this.manager.load(this.entityClass, results) as any; 173 | } else { 174 | results = results.map(res => this.manager.unserializeSimpleValue(res)); 175 | } 176 | for (const res of results) { 177 | yield res; 178 | } 179 | } 180 | } 181 | } -------------------------------------------------------------------------------- /src/Metadata/Metadata.ts: -------------------------------------------------------------------------------- 1 | import { MetadataError } from "../Errors/Errors"; 2 | /** 3 | * Metadata key to mark class to be stored as redis hash 4 | */ 5 | export const REDIS_ENTITY = Symbol("Redis Hash"); 6 | 7 | /** 8 | * Metadata key to store redis properties metadatas 9 | */ 10 | export const REDIS_PROPERTIES = Symbol("Redis properties"); 11 | 12 | /** 13 | * Metadata key to keep serialized initial value of property from redis. For collections this will be link to the corresponding hashmap/set 14 | */ 15 | export const REDIS_VALUE = Symbol("Redis initial value"); 16 | 17 | /** 18 | * Metadata key to keep the initial state of redis collections (map/set) 19 | */ 20 | export const REDIS_COLLECTION_VALUE = Symbol("Redis initial collection value"); 21 | 22 | 23 | export type PropertyMetadata = SimplePropertyMetadata | RelationPropertyMetadata; 24 | 25 | /** 26 | * Hash simple property metadata 27 | * 28 | * @export 29 | * @interface SimplePropertyMetadata 30 | */ 31 | export interface SimplePropertyMetadata { 32 | /** 33 | * Property name in class 34 | */ 35 | propertyName: string; 36 | /** 37 | * Property name stored in redis 38 | */ 39 | propertyRedisName: string; 40 | /** 41 | * Property type 42 | */ 43 | propertyType: any; 44 | /** 45 | * True if property must be identify column 46 | */ 47 | isIdentifyColumn: boolean; 48 | /** 49 | * Relation flag 50 | */ 51 | isRelation: false; 52 | } 53 | 54 | export type RelationTypeFunc = (type?: any) => Function | Function[]; 55 | 56 | /** 57 | * Relation options 58 | * 59 | * @export 60 | * @interface RelationOptions 61 | */ 62 | export interface RelationOptions { 63 | /** 64 | * Property name in redis 65 | */ 66 | propertyName?: string; 67 | /** 68 | * True to automatically create new hash for relation 69 | */ 70 | cascadeInsert?: boolean; 71 | /** 72 | * True to automatically update corresponding relation hash 73 | */ 74 | cascadeUpdate?: boolean; 75 | } 76 | 77 | /** 78 | * Hash relation property metadata 79 | * 80 | * @export 81 | * @interface RelationPropertyMetadata 82 | */ 83 | export interface RelationPropertyMetadata { 84 | /** 85 | * Property name in class 86 | */ 87 | propertyName: string; 88 | /** 89 | * Property name stored in redis 90 | */ 91 | propertyRedisName: string; 92 | /** 93 | * Property type. 94 | * For set property relation it will be Set itself 95 | * For single property relation it will be equal to relationType 96 | */ 97 | propertyType: any; 98 | /** 99 | * Relation flag 100 | */ 101 | isRelation: true; 102 | /** 103 | * Relation type. Must be type of related entity 104 | */ 105 | relationTypeFunc: RelationTypeFunc; 106 | /** 107 | * Relation options 108 | */ 109 | relationOptions: RelationOptions; 110 | } 111 | 112 | /** 113 | * Return true if given object is redis entity like 114 | * 115 | * @export 116 | * @param entity 117 | * @returns 118 | */ 119 | export function isRedisEntity(entity: object): boolean { 120 | if (typeof entity !== "object" || entity === null) { 121 | return false; 122 | } 123 | const metadata = Reflect.getMetadata(REDIS_ENTITY, entity.constructor); 124 | return !!metadata; 125 | } 126 | 127 | /** 128 | * Return entity name 129 | * 130 | * @export 131 | * @param entityOrEntityClass 132 | * @returns 133 | */ 134 | export function getEntityName(entityOrEntityClass: object | Function): string | undefined { 135 | const metadata = Reflect.getMetadata(REDIS_ENTITY, typeof entityOrEntityClass === "function" ? entityOrEntityClass : entityOrEntityClass.constructor); 136 | return metadata; 137 | } 138 | 139 | /** 140 | * Return entity id 141 | * 142 | * @export 143 | * @param entity 144 | * @returns 145 | */ 146 | export function getEntityId(entity: { [key: string]: any }): string | number | undefined { 147 | if (!isRedisEntity(entity)) { 148 | return undefined; 149 | } 150 | const metadatas: PropertyMetadata[] = Reflect.getMetadata(REDIS_PROPERTIES, entity.constructor); 151 | const idMetadata = metadatas && metadatas.find(val => !val.isRelation && val.isIdentifyColumn); 152 | if (!idMetadata) { 153 | return undefined; 154 | } 155 | if (typeof entity[idMetadata.propertyName] === "number" || typeof entity[idMetadata.propertyName] === "string") { 156 | return entity[idMetadata.propertyName]; 157 | } 158 | return undefined; 159 | } 160 | 161 | /** 162 | * Returns redis properties metadata 163 | * 164 | * @export 165 | * @param entityOrEntityClass 166 | * @returns 167 | */ 168 | export function getEntityProperties(entityOrEntityClass: object | Function): PropertyMetadata[] | undefined { 169 | return Reflect.getMetadata(REDIS_PROPERTIES, typeof entityOrEntityClass === "function" ? entityOrEntityClass : entityOrEntityClass.constructor); 170 | } 171 | 172 | /** 173 | * Return full hash id 174 | * 175 | * @export 176 | * @param entityOrEntityClass 177 | * @param [hashId] 178 | * @returns 179 | */ 180 | export function getEntityFullId(entityOrEntityClass: object | Function, hashId?: string | number): string | undefined { 181 | const name = getEntityName(entityOrEntityClass); 182 | const id = typeof hashId !== "undefined" 183 | ? hashId 184 | : typeof entityOrEntityClass === "object" 185 | ? getEntityId(entityOrEntityClass) 186 | : undefined; 187 | if (name && typeof id !== "undefined") { 188 | return `e:${name}:${id}`; 189 | } 190 | } 191 | 192 | /** 193 | * Return relation type from relation type func from metadata 194 | * 195 | * @export 196 | * @param entityOrEntityClass 197 | * @param metadata 198 | * @returns 199 | */ 200 | export function getRelationType(entityOrEntityClass: object | Function, metadata: RelationPropertyMetadata): Function { 201 | const entityName = typeof entityOrEntityClass === "function" ? entityOrEntityClass : entityOrEntityClass.constructor; 202 | if (!metadata.isRelation) { 203 | throw new MetadataError(entityName, `${metadata.propertyName} is not a relation`); 204 | } 205 | const typeOrTypes = metadata.relationTypeFunc(); 206 | const type = Array.isArray(typeOrTypes) ? typeOrTypes[0] : typeOrTypes; 207 | 208 | if (typeof type !== "function") { 209 | throw new MetadataError(entityName, `Invalid relation type ${type} for property ${metadata.propertyName}`); 210 | } 211 | if (typeof Reflect.getMetadata(REDIS_ENTITY, type) === "undefined") { 212 | throw new MetadataError(entityName, `Relation entity ${type.name} must be decorated with @Entity`); 213 | } 214 | return type; 215 | } -------------------------------------------------------------------------------- /src/Persistence/__tests__/__snapshots__/RedisManager_Spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Remove Removes entity 1`] = ` 4 | Array [ 5 | null, 6 | null, 7 | Array [], 8 | ] 9 | `; 10 | 11 | exports[`Remove Removes entity 2`] = ` 12 | Array [ 13 | Object { 14 | "id": "i:1", 15 | "map": "m:e:A:1:map", 16 | "prop1": "s:abc", 17 | "set": "a:e:A:1:set", 18 | }, 19 | Object { 20 | "i:1": "s:uno", 21 | "i:2": "s:dos", 22 | }, 23 | ] 24 | `; 25 | 26 | exports[`Save Doesn't delete relations in maps and sets if skipped them for loading 1`] = ` 27 | Object { 28 | "i:1": "e:Rel:1", 29 | "i:2": "e:Rel:2", 30 | } 31 | `; 32 | 33 | exports[`Save Doesn't touch relation if it was skipped for loading 1`] = ` 34 | Object { 35 | "id": "i:1", 36 | "prop1": "s:abc", 37 | } 38 | `; 39 | 40 | exports[`Save Doesn't update relations in maps/sets without cascade update 1`] = ` 41 | Object { 42 | "id": "i:1", 43 | "prop1": "s:test", 44 | } 45 | `; 46 | 47 | exports[`Save Doesn't update single relation without cascade update 1`] = `Array []`; 48 | 49 | exports[`Save Doesn't update single relation without cascade update 2`] = ` 50 | Object { 51 | "id": "i:1", 52 | "prop1": "s:abc", 53 | } 54 | `; 55 | 56 | exports[`Save Saves cyclic relation with cascade inserting 1`] = ` 57 | Array [ 58 | Object { 59 | "b": "e:B:1", 60 | "id": "i:1", 61 | "prop": "s:I'm a", 62 | }, 63 | Object { 64 | "a": "e:A:1", 65 | "id": "i:1", 66 | "prop": "s:I'm b", 67 | }, 68 | ] 69 | `; 70 | 71 | exports[`Save Saves multiple cyclic relations with cascade insert 1`] = ` 72 | Array [ 73 | Object { 74 | "id": "s:1", 75 | "myMap": "m:e:A:1:myMap", 76 | "mySet": "a:e:A:1:mySet", 77 | }, 78 | Object { 79 | "i:1": "e:AnotherRel:1", 80 | "i:2": "e:AnotherRel:2", 81 | }, 82 | Object { 83 | "id": "i:1", 84 | "prop1": "s:uno", 85 | "rel2": "e:AnotherRel:1", 86 | }, 87 | Object { 88 | "id": "i:2", 89 | "prop1": "s:dos", 90 | "rel2": "e:AnotherRel:1", 91 | }, 92 | Object { 93 | "id": "i:3", 94 | "prop1": "s:tres", 95 | "rel2": "e:AnotherRel:2", 96 | }, 97 | Object { 98 | "id": "i:4", 99 | "prop1": "s:cuatro", 100 | "rel2": "e:AnotherRel:2", 101 | }, 102 | Object { 103 | "a": "e:A:1", 104 | "id": "i:1", 105 | }, 106 | Object { 107 | "a": "e:A:1", 108 | "id": "i:2", 109 | }, 110 | ] 111 | `; 112 | 113 | exports[`Save Saves multiple relations with cascade insert 1`] = ` 114 | Array [ 115 | Object { 116 | "id": "s:1", 117 | "myMap": "m:e:A:1:myMap", 118 | "mySet": "a:e:A:1:mySet", 119 | }, 120 | Object { 121 | "i:1": "e:Rel:3", 122 | "i:2": "e:Rel:4", 123 | }, 124 | Object { 125 | "id": "i:1", 126 | "prop1": "s:uno", 127 | }, 128 | Object { 129 | "id": "i:2", 130 | "prop1": "s:dos", 131 | }, 132 | Object { 133 | "id": "i:3", 134 | "prop1": "s:tres", 135 | }, 136 | Object { 137 | "id": "i:4", 138 | "prop1": "s:cuatro", 139 | }, 140 | ] 141 | `; 142 | 143 | exports[`Save Saves only changed properties 1`] = ` 144 | Array [ 145 | Array [ 146 | "srem", 147 | "a:e:A:1:set1", 148 | "i:1", 149 | ], 150 | Array [ 151 | "sadd", 152 | "a:e:A:1:set1", 153 | "i:4", 154 | ], 155 | Array [ 156 | "hdel", 157 | "e:A:1", 158 | "prop1", 159 | ], 160 | Array [ 161 | "hmset", 162 | "e:A:1", 163 | "prop3", 164 | "d:1447146610000", 165 | "prop4", 166 | "j:{\\"a\\":\\"abc\\",\\"d\\":8}", 167 | "prop5", 168 | "s:abcdef", 169 | ], 170 | Array [ 171 | "hdel", 172 | "m:e:A:1:map1", 173 | "s:1", 174 | ], 175 | Array [ 176 | "hmset", 177 | "m:e:A:1:map1", 178 | "i:2", 179 | "s:number2", 180 | ], 181 | ] 182 | `; 183 | 184 | exports[`Save Saves only changed properties 2`] = ` 185 | Array [ 186 | Object { 187 | "id": "i:1", 188 | "map1": "m:e:A:1:map1", 189 | "prop3": "d:1447146610000", 190 | "prop4": "j:{\\"a\\":\\"abc\\",\\"d\\":8}", 191 | "prop5": "s:abcdef", 192 | "set1": "a:e:A:1:set1", 193 | }, 194 | Object { 195 | "i:1": "s:number1", 196 | "i:2": "s:number2", 197 | "i:3": "d:1478769010000", 198 | }, 199 | ] 200 | `; 201 | 202 | exports[`Save Saves only changed properties 3`] = ` 203 | Array [ 204 | Array [ 205 | "del", 206 | "a:e:A:1:set1", 207 | ], 208 | Array [ 209 | "del", 210 | "m:e:A:1:map1", 211 | ], 212 | Array [ 213 | "hdel", 214 | "e:A:1", 215 | "set1", 216 | "map1", 217 | ], 218 | ] 219 | `; 220 | 221 | exports[`Save Saves only changed properties 4`] = ` 222 | Array [ 223 | Object { 224 | "id": "i:1", 225 | "prop3": "d:1447146610000", 226 | "prop4": "j:{\\"a\\":\\"abc\\",\\"d\\":8}", 227 | "prop5": "s:abcdef", 228 | }, 229 | null, 230 | ] 231 | `; 232 | 233 | exports[`Save Saves only links to multiple relations without cascade insert 1`] = ` 234 | Array [ 235 | Object { 236 | "id": "s:1", 237 | "myMap": "m:e:A:1:myMap", 238 | "mySet": "a:e:A:1:mySet", 239 | }, 240 | Object { 241 | "i:1": "e:Rel:3", 242 | "i:2": "e:Rel:4", 243 | }, 244 | null, 245 | null, 246 | null, 247 | null, 248 | ] 249 | `; 250 | 251 | exports[`Save Saves simple entity 1`] = ` 252 | Array [ 253 | Object { 254 | "boolProp": "b:1", 255 | "id": "i:1", 256 | "map1": "m:e:A:1:map1", 257 | "prop1": "s:abc", 258 | "prop3": "d:1478769010000", 259 | "prop4": "j:{\\"a\\":\\"abc\\",\\"d\\":5}", 260 | "prop5": "null", 261 | "set1": "a:e:A:1:set1", 262 | }, 263 | Object { 264 | "i:1": "s:number1", 265 | "i:2": "b:1", 266 | "i:3": "d:1478769010000", 267 | "s:1": "s:string1", 268 | }, 269 | ] 270 | `; 271 | 272 | exports[`Save Saves single relation with cascade inserting 1`] = ` 273 | Array [ 274 | Object { 275 | "id": "i:1", 276 | "rel": "e:Rel:1", 277 | }, 278 | Object { 279 | "id": "i:1", 280 | "prop": "s:rel", 281 | }, 282 | ] 283 | `; 284 | 285 | exports[`Save Saves single relation without cascade inserting 1`] = ` 286 | Array [ 287 | Object { 288 | "id": "i:1", 289 | "rel": "e:Rel:1", 290 | }, 291 | null, 292 | ] 293 | `; 294 | 295 | exports[`Save Updates relations in maps/sets with cascade update 1`] = ` 296 | Array [ 297 | Array [ 298 | "del", 299 | "m:e:A:1:myMap", 300 | ], 301 | Array [ 302 | "hdel", 303 | "e:A:1", 304 | "myMap", 305 | ], 306 | Array [ 307 | "hmset", 308 | "e:Rel:1", 309 | "prop1", 310 | "s:new test", 311 | ], 312 | ] 313 | `; 314 | 315 | exports[`Save Updates relations in maps/sets with cascade update 2`] = ` 316 | Object { 317 | "id": "i:1", 318 | "prop1": "s:new test", 319 | } 320 | `; 321 | 322 | exports[`Save Updates single relation with with cascade updating 1`] = ` 323 | Array [ 324 | Array [ 325 | "hmset", 326 | "e:A:1", 327 | "prop1", 328 | "s:cde", 329 | ], 330 | ] 331 | `; 332 | 333 | exports[`Save Updates single relation with with cascade updating 2`] = ` 334 | Object { 335 | "id": "i:1", 336 | "prop1": "s:cde", 337 | } 338 | `; 339 | -------------------------------------------------------------------------------- /src/Metadata/__tests__/Metadata_Spec.ts: -------------------------------------------------------------------------------- 1 | import { Entity } from "../../Decorators/Entity"; 2 | import { IdentifyProperty } from "../../Decorators/IdentifyProperty"; 3 | import { Property } from "../../Decorators/Property"; 4 | import { RelationProperty } from "../../Decorators/RelationProperty"; 5 | import { ShouldThrowError } from "../../testutils/ShouldThrowError"; 6 | import { getEntityId, getEntityName, getEntityProperties, getRelationType, isRedisEntity, RelationPropertyMetadata } from "../Metadata"; 7 | 8 | describe("isRedisHash()", () => { 9 | it("returns false for non objects", () => { 10 | expect(isRedisEntity(null as any)).toBeFalsy(); 11 | expect(isRedisEntity(undefined as any)).toBeFalsy(); 12 | expect(isRedisEntity(0 as any)).toBeFalsy(); 13 | expect(isRedisEntity("" as any)).toBeFalsy(); 14 | expect(isRedisEntity(Symbol() as any)).toBeFalsy(); 15 | expect(isRedisEntity(true as any)).toBeFalsy(); 16 | }); 17 | 18 | it("returns false non non redis hash classes instances and simple objects", () => { 19 | class A { } 20 | expect(isRedisEntity(new A())).toBeFalsy(); 21 | expect(isRedisEntity({})).toBeFalsy(); 22 | }); 23 | 24 | it("returns true for redis hash class instances", () => { 25 | @Entity() 26 | class A { } 27 | expect(isRedisEntity(new A())).toBeTruthy(); 28 | }); 29 | }); 30 | 31 | describe("getRedisHashId()", () => { 32 | it("returns undefined for non redis hashes", () => { 33 | class A { } 34 | expect(getEntityId(new A())).toBeUndefined(); 35 | expect(getEntityId({})).toBeUndefined(); 36 | expect(getEntityId(null as any)).toBeUndefined(); 37 | }); 38 | 39 | it("returns undefined if redis hash doesn't contain identify property", () => { 40 | @Entity() 41 | class A { 42 | @Property(String) 43 | public prop: string = "a"; 44 | } 45 | expect(getEntityId(new A())).toBeUndefined(); 46 | }); 47 | 48 | it("returns undefined if identify property is not string or number", () => { 49 | @Entity() 50 | class A { 51 | @IdentifyProperty(String) 52 | public prop: string = new Date() as any; 53 | } 54 | expect(getEntityId(new A())).toBeUndefined(); 55 | }); 56 | 57 | it("returns id", () => { 58 | @Entity() 59 | class A { 60 | @IdentifyProperty(String) 61 | public prop: string = "abc"; 62 | } 63 | @Entity() 64 | class B { 65 | @IdentifyProperty(Number) 66 | public prop: number = 5; 67 | } 68 | expect(getEntityId(new A())).toBe("abc"); 69 | expect(getEntityId(new B())).toBe(5); 70 | }); 71 | }); 72 | 73 | describe("getRedisHashName()", () => { 74 | it("Returns undefined for non hashes", () => { 75 | class A { } 76 | expect(getEntityName(new A())).toBeUndefined(); 77 | expect(getEntityName(A)).toBeUndefined(); 78 | expect(getEntityName({})).toBeUndefined(); 79 | }); 80 | 81 | it("Returns redis hash name either for class or object", () => { 82 | @Entity() 83 | class A { } 84 | 85 | @Entity("test") 86 | class B { } 87 | expect(getEntityName(new A())).toBe("A"); 88 | expect(getEntityName(A)).toBe("A"); 89 | expect(getEntityName(new B())).toBe("test"); 90 | expect(getEntityName(B)).toBe("test"); 91 | }); 92 | }); 93 | 94 | describe("getRedisHashProperties()", () => { 95 | it("Returns properties for even hash instance or hash constructor", () => { 96 | @Entity() 97 | class A { 98 | @IdentifyProperty() 99 | public id: number; 100 | @Property() 101 | public prop2: string; 102 | } 103 | expect(getEntityProperties(A)).toMatchSnapshot(); 104 | expect(getEntityProperties(new A())).toMatchSnapshot(); 105 | }); 106 | 107 | it("Returns undefined for non hashes", () => { 108 | class A { } 109 | expect(getEntityProperties({})).toBeUndefined(); 110 | expect(getEntityProperties(A)).toBeUndefined(); 111 | }); 112 | }); 113 | 114 | describe("getRelationType", () => { 115 | it("Throws if given metadata is not a relation", () => { 116 | @Entity() 117 | class A { 118 | @IdentifyProperty() 119 | public id: number; 120 | 121 | @Property() 122 | public test: string; 123 | } 124 | 125 | const metadata = getEntityProperties(A)!.find(m => m.propertyName === "test") as RelationPropertyMetadata; 126 | 127 | try { 128 | getRelationType(A, metadata); 129 | throw new ShouldThrowError(); 130 | } catch (e) { 131 | if (e instanceof ShouldThrowError) { throw e; } 132 | } 133 | }); 134 | 135 | it("Throws if relation type is not a function", () => { 136 | @Entity() 137 | class A { 138 | @IdentifyProperty() 139 | public id: number; 140 | 141 | @RelationProperty(type => "" as any) 142 | public test: string; 143 | 144 | @RelationProperty(type => ["" as any, String]) 145 | public test2: string; 146 | } 147 | 148 | const metadata = getEntityProperties(A)!.find(m => m.propertyName === "test") as RelationPropertyMetadata; 149 | const metadata2 = getEntityProperties(A)!.find(m => m.propertyName === "test2") as RelationPropertyMetadata; 150 | 151 | try { 152 | getRelationType(A, metadata); 153 | throw new ShouldThrowError(); 154 | } catch (e) { 155 | if (e instanceof ShouldThrowError) { throw e; } 156 | } 157 | try { 158 | getRelationType(A, metadata2); 159 | throw new ShouldThrowError(); 160 | } catch (e) { 161 | if (e instanceof ShouldThrowError) { throw e; } 162 | } 163 | }); 164 | 165 | it("Throws if relation class is not entity", () => { 166 | class Rel { } 167 | 168 | @Entity() 169 | class A { 170 | @IdentifyProperty() 171 | public id: number; 172 | 173 | @RelationProperty(type => Rel) 174 | public test: Rel; 175 | } 176 | 177 | const metadata = getEntityProperties(A)!.find(m => m.propertyName === "test") as RelationPropertyMetadata; 178 | try { 179 | getRelationType(A, metadata); 180 | throw new ShouldThrowError(); 181 | } catch (e) { 182 | if (e instanceof ShouldThrowError) { throw e; } 183 | } 184 | }); 185 | 186 | it("Returns relation type", () => { 187 | @Entity() 188 | class B { 189 | @IdentifyProperty() 190 | public id: number; 191 | } 192 | 193 | @Entity() 194 | class A { 195 | @IdentifyProperty() 196 | public id: number; 197 | 198 | @RelationProperty(type => B) 199 | public test: B; 200 | 201 | @RelationProperty(type => [B, Set]) 202 | public test2: Set; 203 | } 204 | 205 | const metadata = getEntityProperties(A)!.find(m => m.propertyName === "test") as RelationPropertyMetadata; 206 | const metadata2 = getEntityProperties(A)!.find(m => m.propertyName === "test2") as RelationPropertyMetadata; 207 | 208 | expect(getRelationType(A, metadata)).toBe(B); 209 | expect(getRelationType(A, metadata2)).toBe(B); 210 | }); 211 | }); -------------------------------------------------------------------------------- /src/Connection/Connection.ts: -------------------------------------------------------------------------------- 1 | import { AlreadyConnectedError } from "../Errors/Errors"; 2 | import { RedisManager } from "../Persistence/RedisManager"; 3 | import { EntitySubscriberInterface } from "../Subscriber/EntitySubscriberInterface"; 4 | import { PubSubSubscriberInterface } from "../Subscriber/PubSubSubscriberInterface"; 5 | import { getFromContainer } from "../utils/Container"; 6 | import * as redis from "../utils/PromisedRedis"; 7 | 8 | export interface ConnectionOptions extends redis.ClientOpts { 9 | /** 10 | * Array of entity subscribers 11 | */ 12 | entitySubscribers?: Array<{ new(...args: any[]): EntitySubscriberInterface }>; 13 | /** 14 | * PubSub subscriber 15 | */ 16 | pubSubSubscriber?: { new(...args: any[]): PubSubSubscriberInterface }; 17 | 18 | } 19 | 20 | export class Connection { 21 | /** 22 | * Manager instance 23 | */ 24 | public manager: RedisManager; 25 | 26 | /** 27 | * Connection status 28 | */ 29 | public isConnected: boolean = false; 30 | 31 | /** 32 | * Redis client instance 33 | */ 34 | public client: redis.RedisClient; 35 | 36 | /** 37 | * PubSub redis client 38 | * 39 | * @private 40 | */ 41 | private pubsub: redis.RedisClient; 42 | 43 | /** 44 | * Monitor redis client 45 | * 46 | * @private 47 | */ 48 | private monitor?: redis.RedisClient; 49 | 50 | /** 51 | * Options 52 | * 53 | * @private 54 | */ 55 | private options: ConnectionOptions; 56 | 57 | /** 58 | * Creates an instance of Connection. 59 | */ 60 | public constructor() { 61 | this.manager = new RedisManager(this); 62 | } 63 | 64 | /** 65 | * Init connection & connect 66 | * 67 | * @param options 68 | */ 69 | public async connect(options: ConnectionOptions): Promise { 70 | if (this.isConnected) { 71 | throw new AlreadyConnectedError(); 72 | } 73 | this.options = options; 74 | this.client = redis.createClient(options); 75 | this.pubsub = redis.createClient(options); 76 | try { 77 | this.initPubSubListener(); 78 | const subscribers = this.loadEntitySubscribers(this.options.entitySubscribers); 79 | this.manager.assignSubscribers(subscribers); 80 | this.isConnected = true; 81 | } catch (e) { 82 | await this.disconnect(); 83 | throw e; 84 | } 85 | } 86 | 87 | /** 88 | * Disconnect all connections 89 | * 90 | * @returns 91 | */ 92 | public async disconnect(): Promise { 93 | // Stop monitor connection if exist 94 | if (this.monitor) { 95 | await this.monitor.quitAsync(); 96 | } 97 | // Stop pubsub connection 98 | if (this.pubsub) { 99 | await this.pubsub.quitAsync(); 100 | } 101 | // Stop client connection 102 | if (this.client) { 103 | await this.client.quitAsync(); 104 | } 105 | this.isConnected = false; 106 | } 107 | 108 | /** 109 | * Creates monitor connection and executes MONITOR command 110 | */ 111 | public async startMonitoring(logger: (time: number, args: any[], reply: string) => void): Promise { 112 | if (this.monitor) { 113 | return; 114 | } 115 | this.monitor = redis.createClient(this.options); 116 | this.monitor.on("monitor", logger); 117 | await this.monitor.monitorAsync(); 118 | } 119 | 120 | /** 121 | * Stops any monitoring 122 | * 123 | * @returns 124 | */ 125 | public async stopMonitoring(): Promise { 126 | if (this.monitor) { 127 | await this.monitor.quitAsync(); 128 | this.monitor = undefined; 129 | } 130 | } 131 | 132 | /** 133 | * Subscribe for one or more chanels 134 | */ 135 | public async subscribe(channel: string, ...channels: string[]): Promise { 136 | return await this.pubsub.subscribeAsync(channel, ...channels); 137 | } 138 | 139 | /** 140 | * Stop listening for messages posted to the given channels. 141 | */ 142 | public async unsubscribe(channel: string, ...channels: string[]): Promise { 143 | return await this.pubsub.unsubscribeAsync(channel, ...channels); 144 | } 145 | 146 | /** 147 | * Stop listening for messages posted to channels matching the given patterns. 148 | */ 149 | public async psubscribe(pattern: string, ...patterns: string[]): Promise { 150 | return await this.pubsub.psubscribeAsync(pattern, ...patterns); 151 | } 152 | 153 | /** 154 | * Stop listening for messages posted to channels matching the given patterns. 155 | */ 156 | public async punsubscribe(pattern: string, ...channels: string[]): Promise { 157 | return await this.pubsub.punsubscribeAsync(pattern, ...channels); 158 | } 159 | 160 | /** 161 | * Remove all keys from the current database. 162 | */ 163 | public async flushdb(): Promise { 164 | await this.client.flushdbAsync(); 165 | } 166 | 167 | /** 168 | * Execute multiple comamnds inside transaction 169 | * 170 | * @param executor 171 | * @param [atomic=false] 172 | * @returns 173 | */ 174 | public async transaction(executor: (multi: redis.Commands) => void, atomic: boolean = false): Promise { 175 | const multi = this.client.multi(); 176 | executor(multi); 177 | return new Promise((resolve, reject) => { 178 | const func = atomic ? multi.exec_atomic : multi.exec; 179 | func.call(multi, (err: Error, result: any[]) => { 180 | err ? reject(err) : resolve(result); 181 | }); 182 | }); 183 | } 184 | 185 | /** 186 | * Execute multiple commands in batch without transaction 187 | * 188 | * @param executor 189 | * @returns 190 | */ 191 | public async batch(executor: (batch: redis.Commands) => void): Promise { 192 | const batch = this.client.batch(); 193 | executor(batch); 194 | return new Promise((resolve, reject) => { 195 | batch.exec((err, result) => { 196 | err ? reject(err) : resolve(result); 197 | }); 198 | }); 199 | } 200 | 201 | /** 202 | * Init pubsub connection and listeners 203 | * 204 | * @private 205 | */ 206 | private initPubSubListener(): void { 207 | if (typeof this.options.pubSubSubscriber === "function") { 208 | const listener = getFromContainer(this.options.pubSubSubscriber); 209 | if (listener.onMessage) { 210 | this.pubsub.on("message", listener.onMessage.bind(listener)); 211 | } 212 | if (listener.onMessageBuffer) { 213 | this.pubsub.on("message_buffer", listener.onMessageBuffer.bind(listener)); 214 | } 215 | if (listener.onPMessage) { 216 | this.pubsub.on("pmessage", listener.onPMessage.bind(listener)); 217 | } 218 | if (listener.onPMessageBuffer) { 219 | this.pubsub.on("pmessage_buffer", listener.onPMessageBuffer.bind(listener)); 220 | } 221 | if (listener.onSubscribe) { 222 | this.pubsub.on("subscribe", listener.onSubscribe.bind(listener)); 223 | } 224 | if (listener.onPSubscribe) { 225 | this.pubsub.on("psubscribe", listener.onPSubscribe.bind(listener)); 226 | } 227 | if (listener.onUnsubscribe) { 228 | this.pubsub.on("unsubscribe", listener.onUnsubscribe.bind(listener)); 229 | } 230 | if (listener.onPUnsubscribe) { 231 | this.pubsub.on("punsubscribe", listener.onPUnsubscribe.bind(listener)); 232 | } 233 | } 234 | } 235 | 236 | /** 237 | * Instantiate all entity subscribers 238 | * 239 | * @private 240 | * @param subscriberConstructors 241 | * @returns 242 | */ 243 | private loadEntitySubscribers(subscriberConstructors?: Array<{ new(...args: any[]): EntitySubscriberInterface }>): Array> { 244 | const subscribers: Array> = []; 245 | if (subscriberConstructors) { 246 | for (const cl of subscriberConstructors) { 247 | subscribers.push(getFromContainer(cl)); 248 | } 249 | } 250 | return subscribers; 251 | } 252 | } -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Redis ORM for TypeScript 2 | 3 | ## Installation 4 | 5 | ```npm install orm-redis --save``` 6 | 7 | ## In heavy development 8 | 9 | Until the library reaches 0.2 version the API may be unstable and can be changed without any notice 10 | 11 | ## Features: 12 | 13 | * Modern ORM based on decorators & datamapper pattern 14 | * Type binding for numbers, booleans, strings, dates. Objects/arrays are being converted to json strings 15 | * Native support for ES2015 Maps and Sets (Stored as separated hashes/sets in Redis) 16 | * Single field relation support 17 | * Multiple relations support for Maps and Sets 18 | * Relations cascade inserting/updating (But not deleting) 19 | * Entity subscriber support & built-in pubsub subscriber 20 | * Operation optimized - writes to redis only changed values 21 | * Lazy maps/sets support 22 | * Optimistic locking (WIP) 23 | 24 | ## Requirments 25 | 26 | * TypeScript 2.3 or greater 27 | * Node 7.* 28 | * Symbol.asyncIterator polyfill to iterate over lazy maps/sets 29 | 30 | ## Setup 31 | 32 | Enable ```emitDecoratorMetadata``` and ```experimentalDecorators``` in your tsconfig.json 33 | 34 | Install ```reflect-metadata``` library and import it somewhere in your project: 35 | 36 | ``` 37 | npm install reflect-metadata --save 38 | 39 | // in index.ts 40 | import "reflect-metadata"; 41 | ``` 42 | 43 | If you're going to use lazy maps/sets then you need ```Symbol.asyncIterator``` polyfill. This can be easily achived by writing this somewhere in app: 44 | ```ts 45 | (Symbol as any).asyncIterator = (Symbol as any).asyncIterator || Symbol.for("Symbol.asyncIterator"); 46 | ``` 47 | 48 | ## Examples 49 | 50 | ### Basic example 51 | 52 | ```ts 53 | import { createConnection, Entity, Property, IdentifyProperty } from "orm-redis"; 54 | 55 | @Entity() 56 | class MyEntity { 57 | // You must have one identify property. Supported only string and numbers 58 | @IdentifyProperty() 59 | public id: number; 60 | 61 | @Property() 62 | public myString: string; 63 | 64 | @Property() 65 | public myNumber: number; 66 | 67 | @Property() 68 | public myDate: Date; 69 | 70 | @Property() 71 | public myObj: object; // will be stored in json 72 | 73 | @Property(Set) 74 | public mySet: Set = new Set(); // uses redis sets 75 | 76 | @Property(Map) 77 | public myMap: Map = new Map(); // uses redis maps 78 | } 79 | 80 | createConnection({ 81 | host: process.env.REDIS_HOST, 82 | port: process.env.REDIS_PORT 83 | }).then(async connection => { 84 | const entity = new MyEntity(); 85 | entity.id = 1; 86 | entity.myString = "abc"; 87 | entity.myNumber = 1; 88 | entity.myDate = new Date(); 89 | // 1 and "1" keys are different for sets and maps 90 | entity.mySet.add(1).add("1"); 91 | entity.myMap.set(1, true).set("1", false); 92 | 93 | await connection.manager.save(entity); 94 | 95 | // Load entity 96 | const stored = await connection.manager.load(MyEntity, 1): 97 | stored.id; // 1 98 | stored.myDate; // Date object 99 | stored.myMap; // Map { 1: true, "1": false } 100 | stored.mySet; // Set [ 1, "1" ] 101 | 102 | stored.myMap.delete(1); 103 | stored.myMap.set(3, "test"); 104 | stored.myNumber = 10; 105 | 106 | // Save changes. Will trigger persistence operation only for changed keys 107 | await connection.manager.save(stored); 108 | 109 | // Delete 110 | await connection.manager.delete(stored); 111 | }).catch(console.error); 112 | ``` 113 | 114 | ### Relations 115 | 116 | ORM supports both single relations (linked to a single property) and multiple relations (linked to Set/Map) 117 | 118 | ```ts 119 | 120 | @Entity() 121 | class Relation { 122 | @IdentifyProperty() 123 | public id: number; 124 | 125 | @Property() 126 | public relProp: string; 127 | } 128 | 129 | @Entity() 130 | class Owner { 131 | @IdentifyProperty() 132 | public id: string; 133 | 134 | @RelationProperty(type => Relation, { cascadeInsert: true, cascadeUpdate: true }) 135 | public rel: Relation; 136 | 137 | @RelationProperty(type => [Relation, Set], { cascadeInsert: true }) 138 | public relationSet: Set = new Set(); 139 | 140 | @RelationProperty(type => [Relation, Map], { cascadeInsert: true }) 141 | public relationMap: Map = new Map(); 142 | } 143 | 144 | const rel1 = new Relation(); 145 | rel1.id = 1; 146 | const rel2 = new Relation(); 147 | rel2.id = 2; 148 | const owner = new Owner(); 149 | owner.id = "test"; 150 | owner.rel = rel1; 151 | owner.relationSet.add(rel1); 152 | owner.relationMap.set(10, rel1); 153 | owner.relationMap.set(12, rel2); 154 | 155 | // Cascading insert will save all relations too in object 156 | await manager.save(owner); 157 | 158 | // Get and eager load all relations, including all inner relations (if presented) 159 | const loaded = await manager.load(Owner, "test"); 160 | loaded.rel.relProp = "test"; 161 | // If cascadeUpdate was set then will trigger update operation for Relation entity 162 | await manager.save(loaded); 163 | 164 | // Don't load relations for properties rel and relationMap 165 | const another = await manager.load(Owner, "test", /* skip relations */ ["rel", "relationMap"]); 166 | ``` 167 | 168 | *NOTE: ORM DOESN'T support cascade delete operation now, you must delete your relations manually* 169 | 170 | ```ts 171 | @Entity() 172 | class Relation { 173 | @IdentifyProperty() 174 | public id: number; 175 | } 176 | 177 | @Entity() 178 | class Owner { 179 | @IdentifyProperty() 180 | public id: number; 181 | 182 | @RelationProperty(type => [Relation, Set]) 183 | public setRel: Set = new Set(); 184 | } 185 | 186 | // To clean owner object with all linked relations: 187 | const owner = await manager.load(Owner, 1); 188 | for (const rel of owner.setRel) { 189 | await manager.delete(rel); 190 | } 191 | await manager.delete(owner); 192 | ``` 193 | 194 | ### Lazy collections 195 | 196 | By default all sets/maps are being loaded eagerly. If your map/set in redis is very big, it can take some time to load, especially for relation sets/maps. 197 | You can use lazy sets/maps in this case: 198 | 199 | ```ts 200 | import { LazySet, LazyMap } from "orm-redis"; 201 | 202 | @Entity() 203 | class Ent { 204 | @IdentifyProperty() 205 | public id: number; 206 | 207 | @Property() 208 | public set: LazySet = new LazySet(); 209 | 210 | @Property() 211 | public map: LazyMap = new LazyMap() 212 | } 213 | 214 | const ent = new Ent(); 215 | ent.id = 1; 216 | // Won't be saved until calling manager.save() for new entities 217 | await ent.set.add(1); 218 | await ent.map.set(1, true); 219 | 220 | await manager.save(ent); 221 | 222 | // Immediately saved to set in redis now 223 | await ent.set.add(2); 224 | 225 | // Use asyncronyous iterator available in TS 2.3+ 226 | for await (const v of ent.set.values()) { 227 | // do something with v 228 | } 229 | console.log(await ent.set.size()); // 2 230 | 231 | const anotherEnt = await manager.load(Ent, 2); 232 | for await (const [key, val] of anotherEnt.map.keysAndValues()) { 233 | // [1, true] 234 | } 235 | 236 | @Entity() 237 | class Rel { 238 | @IdentifyProperty() 239 | public id: number; 240 | } 241 | 242 | @Entity() 243 | class AnotherEnt { 244 | @IdentifyProperty() 245 | public id: number; 246 | 247 | @RelationProperty(type => [Rel, LazyMap], { cascadeInsert: true }) // same rules as for ordinary relation map/set for cascading ops 248 | public map: LazyMap = new LazyMap() 249 | } 250 | 251 | const rel = new Rel(); 252 | rel.id = 1; 253 | const anotherEnt = new AnotherEnt(); 254 | anotherEnt.id = 1; 255 | anotherEnt.map.set(1, rel); 256 | 257 | await manager.save(anotherEnt); 258 | 259 | const savedEnt = await manager.load(AnotherEnt, 1); 260 | await savedEnt.map.get(1); // Rel { id: 1 } 261 | ``` 262 | 263 | Asyncronyous iterators are using ```SCAN``` redis command so they suitable for iterating over big collections. 264 | 265 | LazyMap/LazySet after saving entity/loading entity are being converted to specialized RedisLazyMap/RedisLazySet. 266 | 267 | Also it's possible to use RedisLazyMap/RedisLazySet directly: 268 | 269 | ```ts 270 | const map = new RedisLazyMap(/* redis map id */"someMapId", /* redis manager */ connection.manager); 271 | await map.set(1, true); 272 | await map.set(2, false); 273 | 274 | for await (const [key, val] of map.keysAndValues()) { 275 | // [1, true], [2, false] 276 | } 277 | 278 | ``` 279 | 280 | ### Subscribers 281 | 282 | 283 | ### PubSub 284 | 285 | 286 | ### Connection -------------------------------------------------------------------------------- /src/Connection/__tests__/Connection_Spec.ts: -------------------------------------------------------------------------------- 1 | import * as redis from "redis"; 2 | import { AlreadyConnectedError } from "../../Errors/Errors"; 3 | import { EntitySubscriberInterface } from "../../Subscriber/EntitySubscriberInterface"; 4 | import { PubSubSubscriberInterface } from "../../Subscriber/PubSubSubscriberInterface"; 5 | import { cleanRedisConnection, createRawRedisClient, createRedisConnection } from "../../testutils/redis"; 6 | import { ShouldThrowError } from "../../testutils/ShouldThrowError"; 7 | import { Connection } from "../Connection"; 8 | 9 | describe("With default connection", () => { 10 | let conn: Connection; 11 | beforeAll(async () => { 12 | conn = await createRedisConnection(); 13 | }); 14 | 15 | afterEach(async () => { 16 | await conn.flushdb(); 17 | }); 18 | 19 | afterAll(async () => { 20 | await cleanRedisConnection(conn); 21 | }); 22 | 23 | it("Throws error if trying to connect again", async () => { 24 | try { 25 | await conn.connect({}); 26 | throw new ShouldThrowError(); 27 | } catch (e) { 28 | if (e instanceof ShouldThrowError) { throw e; } 29 | expect(e).toBeInstanceOf(AlreadyConnectedError); 30 | } 31 | }); 32 | 33 | describe("Disconnects and throw error", () => { 34 | it("When failed to init pub sub listener", async () => { 35 | class TestPubSub { 36 | public constructor() { 37 | throw new Error(); 38 | } 39 | } 40 | try { 41 | const client = await createRedisConnection({ 42 | pubSubSubscriber: TestPubSub 43 | }); 44 | // Need to disconnect in case if createConnection won't throw 45 | await client.disconnect(); 46 | throw new ShouldThrowError(); 47 | } catch (e) { 48 | if (e instanceof ShouldThrowError) { throw e; } 49 | } 50 | }); 51 | 52 | it("When failed to init entity subscribers", async () => { 53 | class TestEntitySubscriber implements EntitySubscriberInterface { 54 | public constructor() { 55 | throw new Error(); 56 | } 57 | public listenTo(): Function { return undefined as any; } 58 | } 59 | try { 60 | const client = await createRedisConnection({ 61 | entitySubscribers: [TestEntitySubscriber] 62 | }); 63 | // Need to disconnect in case if createConnection won't throw 64 | await client.disconnect(); 65 | throw new ShouldThrowError(); 66 | } catch (e) { 67 | if (e instanceof ShouldThrowError) { throw e; } 68 | } 69 | }); 70 | }); 71 | 72 | describe("Monitoring", () => { 73 | it("Starts another connection for monigoring", async () => { 74 | const loggerSpy = jest.fn(); 75 | await conn.startMonitoring(loggerSpy); 76 | // need to wait sligtly 77 | await new Promise(resolve => setTimeout(resolve, 300)); 78 | 79 | await conn.flushdb(); 80 | await new Promise(resolve => setTimeout(resolve, 300)); 81 | expect(loggerSpy).toBeCalledWith(expect.any(String), ["flushdb"], expect.any(String)); 82 | }); 83 | 84 | it("Stops monitoring", async () => { 85 | const loggerSpy = jest.fn(); 86 | await conn.startMonitoring(loggerSpy); 87 | await new Promise(resolve => setTimeout(resolve, 200)); 88 | await conn.stopMonitoring(); 89 | 90 | await conn.client.pingAsync("test"); 91 | expect(loggerSpy).not.toBeCalledWith(expect.any(String), ["ping", "test"], expect.any(String)); 92 | }); 93 | }); 94 | 95 | it("Works within transaction", async () => { 96 | await conn.transaction(executor => { 97 | executor.sadd("testlist", "1", "2", "3"); 98 | executor.hmset("somehash", { val1: "test", val2: 5 }); 99 | }); 100 | const testlist = await conn.client.smembersAsync("testlist"); 101 | expect(testlist).toEqual(["1", "2", "3"]); 102 | const testhash = await conn.client.hgetallAsync("somehash"); 103 | expect(testhash).toEqual({ val1: "test", val2: "5" }); 104 | }); 105 | 106 | it("Works within batch", async () => { 107 | await conn.batch(executor => { 108 | executor.sadd("testlist", "1", "2", "3"); 109 | executor.hmset("somehash", { val1: "test", val2: 5 }); 110 | }); 111 | const testlist = await conn.client.smembersAsync("testlist"); 112 | expect(testlist).toEqual(["1", "2", "3"]); 113 | const testhash = await conn.client.hgetallAsync("somehash"); 114 | expect(testhash).toEqual({ val1: "test", val2: "5" }); 115 | }); 116 | }); 117 | 118 | 119 | 120 | describe("PubSub", () => { 121 | let client: redis.RedisClient; 122 | let conn: Connection; 123 | 124 | beforeAll(async () => { 125 | client = await createRawRedisClient(); 126 | }); 127 | 128 | afterEach(async () => { 129 | if (conn) { 130 | await cleanRedisConnection(conn); 131 | } 132 | }); 133 | 134 | afterAll(async () => { 135 | if (client) { 136 | await client.quitAsync(); 137 | } 138 | }); 139 | 140 | it("Listens for message", async () => { 141 | const messages: string[][] = []; 142 | class TestListener implements PubSubSubscriberInterface { 143 | public onMessage: jest.Mock; 144 | public constructor() { 145 | this.onMessage = jest.fn().mockImplementation((channel: string, value: string) => { 146 | messages.push([channel, value]); 147 | }); 148 | } 149 | } 150 | conn = await createRedisConnection({ pubSubSubscriber: TestListener }); 151 | await conn.subscribe("some channel", "some other channel"); 152 | await client.publishAsync("some channel", "some message"); 153 | await client.publishAsync("some other channel", "some message"); 154 | await new Promise(resolve => setTimeout(resolve, 200)); 155 | expect(messages[0]).toEqual(["some channel", "some message"]); 156 | expect(messages[1]).toEqual(["some other channel", "some message"]); 157 | }); 158 | 159 | it("Listens for message with pattern", async () => { 160 | const messages: string[][] = []; 161 | class TestListener implements PubSubSubscriberInterface { 162 | public onPMessage: jest.Mock; 163 | public constructor() { 164 | this.onPMessage = jest.fn().mockImplementation((pattern: string, channel: string, value: string) => { 165 | messages.push([pattern, channel, value]); 166 | }); 167 | } 168 | } 169 | conn = await createRedisConnection({ pubSubSubscriber: TestListener }); 170 | await conn.psubscribe("channel?"); 171 | await client.publishAsync("channel1", "some message"); 172 | await client.publishAsync("channel2", "some message"); 173 | await new Promise(resolve => setTimeout(resolve, 200)); 174 | expect(messages[0]).toEqual(["channel?", "channel1", "some message"]); 175 | expect(messages[1]).toEqual(["channel?", "channel2", "some message"]); 176 | }); 177 | 178 | it("Unsubscribes", async () => { 179 | const messages: string[][] = []; 180 | class TestListener implements PubSubSubscriberInterface { 181 | public onPMessage: jest.Mock; 182 | public onMessage: jest.Mock; 183 | public constructor() { 184 | this.onPMessage = jest.fn().mockImplementation((pattern: string, channel: string, value: string) => { 185 | messages.push([pattern, channel, value]); 186 | }); 187 | this.onMessage = jest.fn().mockImplementation((channel: string, value: string) => { 188 | messages.push([channel, value]); 189 | }); 190 | } 191 | } 192 | conn = await createRedisConnection({ pubSubSubscriber: TestListener }); 193 | await conn.subscribe("channel"); 194 | await conn.psubscribe("test?"); 195 | 196 | await conn.unsubscribe("channel"); 197 | await conn.punsubscribe("test?"); 198 | 199 | await client.publishAsync("channel1", "some message"); 200 | await client.publishAsync("channel2", "some message"); 201 | await new Promise(resolve => setTimeout(resolve, 200)); 202 | expect(messages).toHaveLength(0); 203 | }); 204 | 205 | it("Listens for subscribe/unsubscribe events", async () => { 206 | const messages: string[][] = []; 207 | class TestListener implements PubSubSubscriberInterface { 208 | public onSubscribe: jest.Mock; 209 | public onPSubscribe: jest.Mock; 210 | public onUnsubscribe: jest.Mock; 211 | public onPUnsubscribe: jest.Mock; 212 | public constructor() { 213 | this.onSubscribe = jest.fn((channel: string) => messages.push(["subscribe", channel])); 214 | this.onPSubscribe = jest.fn((channel: string) => messages.push(["psubscribe", channel])); 215 | this.onUnsubscribe = jest.fn((channel: string) => messages.push(["unsubscribe", channel])); 216 | this.onPUnsubscribe = jest.fn((channel: string) => messages.push(["punsubscribe", channel])); 217 | } 218 | } 219 | conn = await createRedisConnection({ pubSubSubscriber: TestListener }); 220 | await conn.subscribe("channel"); 221 | await conn.psubscribe("test?"); 222 | 223 | await conn.unsubscribe("channel"); 224 | await conn.punsubscribe("test?"); 225 | await new Promise(resolve => setTimeout(resolve, 200)); 226 | expect(messages).toHaveLength(4); 227 | expect(messages[0]).toEqual(["subscribe", "channel"]); 228 | expect(messages[1]).toEqual(["psubscribe", "test?"]); 229 | expect(messages[2]).toEqual(["unsubscribe", "channel"]); 230 | expect(messages[3]).toEqual(["punsubscribe", "test?"]); 231 | }); 232 | }); 233 | -------------------------------------------------------------------------------- /src/Collections/RedisLazyMap.ts: -------------------------------------------------------------------------------- 1 | import { getEntityFullId } from "../Metadata/Metadata"; 2 | import { EntityType, RedisManager } from "../Persistence/RedisManager"; 3 | import { LazyMap } from "./LazyMap"; 4 | 5 | export class RedisLazyMap extends LazyMap { 6 | /** 7 | * Manager instance 8 | * 9 | * @private 10 | */ 11 | private manager: RedisManager; 12 | 13 | /** 14 | * Map id 15 | * 16 | * @private 17 | */ 18 | private mapId: string; 19 | 20 | /** 21 | * Entity class 22 | * 23 | * @private 24 | */ 25 | private entityClass?: Function; 26 | 27 | /** 28 | * Cascade inserting 29 | * 30 | * @private 31 | */ 32 | private cascadeInsert: boolean; 33 | 34 | /** 35 | * Creates an instance of RedisLazyMap. 36 | * @param mapId Full map id in redis 37 | * @param manager Manager instance 38 | * @param [entityClass] If passed then map will be treated as entity map and will set/return entities 39 | * @param [cascadeInsert=false] True to automatically save entities 40 | */ 41 | public constructor(mapId: string, manager: RedisManager, entityClass?: EntityType, cascadeInsert: boolean = false) { 42 | super(); 43 | this.manager = manager; 44 | this.mapId = mapId; 45 | this.entityClass = entityClass; 46 | this.cascadeInsert = cascadeInsert; 47 | } 48 | 49 | /** 50 | * Get map size 51 | * 52 | * @returns 53 | */ 54 | public async size(): Promise { 55 | return await this.manager.connection.client.hlenAsync(this.mapId); 56 | } 57 | 58 | /** 59 | * Set value 60 | * 61 | * @param key 62 | * @param value 63 | * @returns 64 | */ 65 | public async set(key: K, value: V): Promise { 66 | const serializedKey = this.manager.serializeSimpleValue(key); 67 | if (serializedKey) { 68 | if (this.entityClass) { 69 | const entityId = getEntityFullId(value as any); 70 | if (!entityId) { 71 | throw new Error(`Unable to get entity id for key: ${key} and class ${this.entityClass.name}`); 72 | } 73 | if (this.cascadeInsert) { 74 | // save entity 75 | await this.manager.save(value as any); 76 | } 77 | // set mapping in map itself 78 | await this.manager.connection.client.hsetAsync(this.mapId, serializedKey, entityId); 79 | } else { 80 | const serializedVal = this.manager.serializeSimpleValue(value); 81 | if (serializedVal) { 82 | this.manager.connection.client.hsetAsync(this.mapId, serializedKey, serializedVal); 83 | } 84 | } 85 | } 86 | } 87 | 88 | /** 89 | * Delete value by key 90 | * 91 | * @param key 92 | * @param [deleteEntity=false] Also delete corresponding entity 93 | * @returns 94 | */ 95 | public async delete(key: K, deleteEntity: boolean = false): Promise { 96 | let res: number; 97 | const serializedKey = this.manager.serializeSimpleValue(key); 98 | if (!serializedKey) { 99 | return false; 100 | } 101 | if (this.entityClass) { 102 | if (deleteEntity) { 103 | const entityId = await this.manager.connection.client.hgetAsync(this.mapId, serializedKey); 104 | if (entityId) { 105 | await this.manager.removeById(this.entityClass, entityId); 106 | } 107 | } 108 | res = await this.manager.connection.client.hdelAsync(this.mapId, serializedKey); 109 | } else { 110 | res = await this.manager.connection.client.hdelAsync(this.mapId, serializedKey); 111 | } 112 | return !!res; 113 | } 114 | 115 | /** 116 | * Check if the map has key 117 | * 118 | * @param key 119 | * @returns 120 | */ 121 | public async has(key: K): Promise { 122 | const serializedKey = this.manager.serializeSimpleValue(key); 123 | if (!serializedKey) { 124 | return false; 125 | } 126 | if (this.entityClass) { 127 | // check also if we have this entity itself 128 | const entityId = await this.manager.connection.client.hgetAsync(this.mapId, serializedKey); 129 | if (!entityId) { 130 | return false; 131 | } 132 | return !! await this.manager.connection.client.existsAsync(entityId); 133 | } else { 134 | // simple checking by checking key 135 | return !! await this.manager.connection.client.hexistsAsync(this.mapId, serializedKey); 136 | } 137 | } 138 | 139 | /** 140 | * Get the value by given key 141 | * 142 | * @param key 143 | * @returns 144 | */ 145 | public async get(key: K): Promise { 146 | const serializedKey = this.manager.serializeSimpleValue(key); 147 | if (!serializedKey) { 148 | return; 149 | } 150 | const val = await this.manager.connection.client.hgetAsync(this.mapId, serializedKey); 151 | if (!val) { 152 | return; 153 | } 154 | // return simple value if not entity map 155 | if (!this.entityClass) { 156 | return this.manager.unserializeSimpleValue(val); 157 | } 158 | // Load entity otherwise 159 | return await this.manager.load(this.entityClass, val) as any; 160 | } 161 | 162 | /** 163 | * Clear map 164 | * 165 | * @param [deleteEntities=false] Also delete entties 166 | * @returns 167 | */ 168 | public async clear(deleteEntities: boolean = false): Promise { 169 | if (this.entityClass && deleteEntities) { 170 | const values = await this.manager.connection.client.hvalsAsync(this.mapId); 171 | await this.manager.removeById(this.entityClass, values); 172 | } 173 | // delete map 174 | await this.manager.connection.client.delAsync(this.mapId); 175 | } 176 | 177 | /** 178 | * Convert map to array 179 | * 180 | * @returns 181 | */ 182 | public async toArray(): Promise> { 183 | const res: Array<[K, V]> = []; 184 | for await (const pair of this.keysAndValues()) { 185 | res.push(pair); 186 | } 187 | return res; 188 | } 189 | 190 | /** 191 | * Iterate over map keys => values 192 | * @param scanCount Redis SCAN's COUNT option 193 | * 194 | * @returns 195 | */ 196 | public async * keysAndValues(scanCount?: number): AsyncIterableIterator<[K, V]> { 197 | const scanOption = scanCount ? ["COUNT", scanCount] : []; 198 | let [cursor, results] = await this.manager.connection.client.hscanAsync(this.mapId, "0", ...scanOption); 199 | const getResults = async (result: string[]): Promise => { 200 | const unserializedKeys = result.reduce((keys, current, index) => { 201 | if (index % 2 === 0) { 202 | keys.push(current); 203 | } 204 | return keys; 205 | }, [] as string[]).map(val => this.manager.unserializeSimpleValue(val)); 206 | let vals: any[] = result.reduce((vals, current, index) => { 207 | if (index % 2) { 208 | vals.push(current); 209 | } 210 | return vals; 211 | }, [] as string[]); 212 | 213 | if (this.entityClass) { 214 | // Loading will return only unique entities and since map can have different keys 215 | // pointed to the same value the we need to preserve the original order 216 | const entities = await this.manager.load(this.entityClass, vals) as any[]; 217 | const entityMap = new Map(); 218 | for (const ent of entities) { 219 | entityMap.set(getEntityFullId(ent)!, ent); 220 | } 221 | vals = vals.map(val => entityMap.get(val)); 222 | } else { 223 | vals = vals.map(val => this.manager.unserializeSimpleValue(val)); 224 | } 225 | 226 | return unserializedKeys.map((key, index) => [key, vals[index]]); 227 | }; 228 | const res = await getResults(results); 229 | for (const keyVal of res) { 230 | yield keyVal; 231 | } 232 | 233 | while (cursor !== "0") { 234 | [cursor, results] = await this.manager.connection.client.hscanAsync(this.mapId, cursor, ...scanOption); 235 | const res = await getResults(results); 236 | for (const keyVal of res) { 237 | yield keyVal; 238 | } 239 | } 240 | } 241 | 242 | /** 243 | * Iterate over map's keys 244 | * @param scanCount Redis SCAN's COUNT option 245 | * 246 | * @returns 247 | */ 248 | public async * keys(scanCount?: number): AsyncIterableIterator { 249 | const scanOption = scanCount ? ["COUNT", scanCount] : []; 250 | let [cursor, results] = await this.manager.connection.client.hscanAsync(this.mapId, "0", ...scanOption); 251 | const getKeysFromResult = (result: string[]): any[] => { 252 | return results.reduce((keys, current, index) => { 253 | if (index % 2 === 0) { 254 | keys.push(current); 255 | } 256 | return keys; 257 | }, [] as string[]).map(val => this.manager.unserializeSimpleValue(val)); 258 | }; 259 | let keys = getKeysFromResult(results); 260 | for (const key of keys) { 261 | yield key; 262 | } 263 | while (cursor !== "0") { 264 | [cursor, results] = await this.manager.connection.client.hscanAsync(this.mapId, cursor, ...scanOption); 265 | keys = getKeysFromResult(results); 266 | for (const key of keys) { 267 | yield key; 268 | } 269 | } 270 | } 271 | 272 | /** 273 | * Iterate over map's values 274 | * @param scanCount Redis SCAN's COUNT option 275 | * 276 | * @returns 277 | */ 278 | public async * values(scanCount?: number): AsyncIterableIterator { 279 | for await (const [, val] of this.keysAndValues(scanCount)) { 280 | yield val; 281 | } 282 | } 283 | } -------------------------------------------------------------------------------- /src/Collections/__tests__/RedisLazySet_Spec.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from "../../Connection/Connection"; 2 | import { Entity } from "../../Decorators/Entity"; 3 | import { IdentifyProperty } from "../../Decorators/IdentifyProperty"; 4 | import { Property } from "../../Decorators/Property"; 5 | import { RedisManager } from "../../Persistence/RedisManager"; 6 | import { cleanRedisConnection, createRedisConnection } from "../../testutils/redis"; 7 | import { RedisTestMonitor } from "../../testutils/RedisTestMonitor"; 8 | import { RedisLazySet } from "../RedisLazySet"; 9 | 10 | let conn: Connection; 11 | let manager: RedisManager; 12 | let monitor: RedisTestMonitor; 13 | beforeAll(async () => { 14 | conn = await createRedisConnection(); 15 | manager = conn.manager; 16 | monitor = await RedisTestMonitor.create(conn); 17 | 18 | }); 19 | 20 | afterEach(async () => { 21 | await monitor.clearMonitorCalls(50); 22 | await conn.flushdb(); 23 | }); 24 | 25 | afterAll(async () => { 26 | await monitor.release(); 27 | await cleanRedisConnection(conn); 28 | }); 29 | 30 | describe("Add", () => { 31 | it("Add simple value", async () => { 32 | const set = new RedisLazySet("a:mySet", manager); 33 | let res = await conn.client.smembersAsync("a:mySet"); 34 | expect(res).toEqual([]); 35 | 36 | await set.add(1); 37 | await set.add(true); 38 | await set.add("test"); 39 | res = await conn.client.smembersAsync("a:mySet"); 40 | expect(res).toEqual(expect.arrayContaining([ 41 | "i:1", 42 | "b:1", 43 | "s:test" 44 | ])); 45 | }); 46 | 47 | it("Add entity with/without cascade insert", async () => { 48 | @Entity() 49 | class Ent { 50 | @IdentifyProperty() 51 | public id: number; 52 | 53 | @Property() 54 | public prop: string = "test prop"; 55 | } 56 | 57 | const set = new RedisLazySet("a:mySet", manager, Ent); 58 | const ent1 = new Ent(); 59 | ent1.id = 1; 60 | const ent2 = new Ent(); 61 | ent2.id = 2; 62 | await set.add(ent1); 63 | await set.add(ent2); 64 | 65 | const setRes = await conn.client.smembersAsync("a:mySet"); 66 | expect(setRes).toEqual(expect.arrayContaining([ 67 | "e:Ent:1", 68 | "e:Ent:2" 69 | ])); 70 | const exists = await Promise.all([ 71 | conn.client.existsAsync("e:Ent:1"), 72 | conn.client.existsAsync("e:Ent:2"), 73 | ]); 74 | expect(exists).toEqual([0, 0]); 75 | 76 | const set2 = new RedisLazySet("a:set2", manager, Ent, true); 77 | await set2.add(ent1); 78 | await set2.add(ent2); 79 | const set2Res = await conn.client.smembersAsync("a:set2"); 80 | expect(set2Res).toEqual(expect.arrayContaining([ 81 | "e:Ent:1", 82 | "e:Ent:2" 83 | ])); 84 | const res = await Promise.all([ 85 | conn.client.hgetallAsync("e:Ent:1"), 86 | conn.client.hgetallAsync("e:Ent:2"), 87 | ]); 88 | expect(res[0]).toEqual({ 89 | id: "i:1", 90 | prop: "s:test prop", 91 | }); 92 | expect(res[1]).toEqual({ 93 | id: "i:2", 94 | prop: "s:test prop", 95 | }); 96 | }); 97 | }); 98 | 99 | describe("Delete", () => { 100 | it("Delete simple value from set", async () => { 101 | await conn.client.saddAsync("a:mySet", "i:1", "i:2", "i:3"); 102 | const set = new RedisLazySet("a:mySet", manager); 103 | await set.delete(1); 104 | await set.delete(2); 105 | await set.delete(10); 106 | 107 | const res = await conn.client.smembersAsync("a:mySet"); 108 | expect(res).toEqual(["i:3"]); 109 | }); 110 | 111 | it("Deletes entity from set and entity itself if requested", async () => { 112 | @Entity() 113 | class Ent { 114 | @IdentifyProperty() 115 | public id: number; 116 | } 117 | const set = new RedisLazySet("a:mySet", manager, Ent, true); 118 | const ent1 = new Ent(); 119 | ent1.id = 1; 120 | const ent2 = new Ent(); 121 | ent2.id = 2; 122 | 123 | await set.add(ent1); 124 | await set.add(ent2); 125 | 126 | let setRes = await conn.client.smembersAsync("a:mySet"); 127 | expect(setRes).toEqual(expect.arrayContaining([ 128 | "e:Ent:1", 129 | "e:Ent:2" 130 | ])); 131 | 132 | await set.delete(ent2); 133 | setRes = await conn.client.smembersAsync("a:mySet"); 134 | expect(setRes).toEqual([ 135 | "e:Ent:1", 136 | ]); 137 | const ent2Res = await conn.client.existsAsync("e:Ent:2"); 138 | expect(ent2Res).toBe(1); 139 | await set.delete(ent1, true); 140 | const ent1Res = await conn.client.existsAsync("e:Ent:1"); 141 | expect(ent1Res).toBe(0); 142 | }); 143 | }); 144 | 145 | describe("Has", () => { 146 | it("Checks for existence of simple value", async () => { 147 | await conn.client.saddAsync("a:mySet", "i:1", "i:2", "i:3"); 148 | const set = new RedisLazySet("a:mySet", manager); 149 | expect(await set.has(1)).toBeTruthy(); 150 | expect(await set.has(2)).toBeTruthy(); 151 | expect(await set.has("2")).toBeFalsy(); 152 | expect(await set.has(10)).toBeFalsy(); 153 | }); 154 | 155 | it("Checks for existence of entity", async () => { 156 | @Entity() 157 | class Ent { 158 | @IdentifyProperty() 159 | public id: number; 160 | 161 | @Property() 162 | public prop: string = "test prop"; 163 | } 164 | const set = new RedisLazySet("a:mySet", manager, Ent); 165 | const ent1 = new Ent(); 166 | ent1.id = 1; 167 | const ent2 = new Ent(); 168 | ent2.id = 2; 169 | const ent3 = new Ent(); 170 | ent3.id = 3; 171 | await set.add(ent1); 172 | await set.add(ent2); 173 | await manager.save(ent3); 174 | 175 | expect(await set.has(ent1)).toBeTruthy(); 176 | expect(await set.has(ent2)).toBeTruthy(); 177 | expect(await set.has(ent3)).toBeFalsy(); 178 | }); 179 | }); 180 | 181 | describe("Size", () => { 182 | it("Returns set size", async () => { 183 | await conn.client.saddAsync("a:mySet", "i:1", "i:2", "i:3"); 184 | const set = new RedisLazySet("a:mySet", manager); 185 | expect(await set.size()).toBe(3); 186 | }); 187 | }); 188 | 189 | describe("clear", () => { 190 | it("Deletes simple set", async () => { 191 | const set = new RedisLazySet("a:mySet", manager); 192 | 193 | await set.add(1); 194 | await set.add(true); 195 | await set.add("test"); 196 | await set.clear(); 197 | const res = await conn.client.smembersAsync("a:mySet"); 198 | expect(res).toEqual([]); 199 | }); 200 | 201 | it("Deletes entity set with entities if requested", async () => { 202 | @Entity() 203 | class A { 204 | @IdentifyProperty() 205 | public id: number; 206 | } 207 | 208 | const set1 = new RedisLazySet("a:mySet", manager, A, true); 209 | const set2 = new RedisLazySet("a:mySet2", manager, A, true); 210 | const a1 = new A(); 211 | a1.id = 1; 212 | const a2 = new A(); 213 | a2.id = 2; 214 | const a3 = new A(); 215 | a3.id = 3; 216 | const a4 = new A(); 217 | a4.id = 4; 218 | 219 | await set1.add(a1); 220 | await set1.add(a2); 221 | 222 | await set2.add(a3); 223 | await set2.add(a4); 224 | 225 | await set1.clear(); 226 | await set2.clear(true); 227 | 228 | const setsExists = await Promise.all([ 229 | conn.client.existsAsync("a:mySet"), 230 | conn.client.existsAsync("a:mySet2"), 231 | ]); 232 | expect(setsExists).toEqual([0, 0]); 233 | 234 | const aExists = await Promise.all([ 235 | conn.client.existsAsync("e:A:1"), 236 | conn.client.existsAsync("e:A:2"), 237 | conn.client.existsAsync("e:A:3"), 238 | conn.client.existsAsync("e:A:4"), 239 | ]); 240 | expect(aExists).toEqual([1, 1, 0, 0]); 241 | }); 242 | }); 243 | 244 | describe("toArray", () => { 245 | it("Returns all set values in array", async () => { 246 | const set = new RedisLazySet("a:mySet", manager); 247 | 248 | await set.add(1); 249 | await set.add(true); 250 | await set.add("test"); 251 | const arr = await set.toArray(); 252 | expect(arr).toEqual(expect.arrayContaining([1, true, "test"])); 253 | }); 254 | }); 255 | 256 | describe("Values", () => { 257 | it("Iterates over simple set values", async () => { 258 | const prefill: number[] = []; 259 | for (let i = 0; i < 1000; i++) { 260 | prefill.push(i); 261 | } 262 | await conn.client.saddAsync("a:mySet", prefill.map(val => `i:${val}`)); 263 | const set = new RedisLazySet("a:mySet", manager); 264 | 265 | await monitor.clearMonitorCalls(150); 266 | 267 | const gettedValues: number[] = []; 268 | 269 | for await (const val of set.values()) { 270 | expect(typeof val).toBe("number"); 271 | gettedValues.push(val); 272 | } 273 | 274 | expect(gettedValues).toHaveLength(1000); 275 | expect(gettedValues).toEqual(expect.arrayContaining(prefill)); 276 | await monitor.wait(100); 277 | // req[0] is sscan 278 | // req[1] is a:mySet 279 | expect(monitor.requests.map(req => [req[0], [req[1]]]).length).toBeGreaterThan(10); 280 | }); 281 | 282 | it("Iterates over entities", async () => { 283 | @Entity() 284 | class Ent { 285 | @IdentifyProperty() 286 | public id: number; 287 | 288 | @Property() 289 | public prop: string = "test prop"; 290 | } 291 | const idsInSet: string[] = []; 292 | for (let i = 0; i < 50; i++) { 293 | const ent = new Ent(); 294 | ent.id = i; 295 | await manager.save(ent); 296 | idsInSet.push(`e:Ent:${i}`); 297 | } 298 | await conn.client.saddAsync("a:mySet", idsInSet); 299 | const set = new RedisLazySet("a:mySet", manager, Ent); 300 | 301 | await monitor.clearMonitorCalls(150); 302 | const entites: Ent[] = []; 303 | for await (const val of set.values()) { 304 | entites.push(val); 305 | } 306 | expect(entites).toHaveLength(50); 307 | expect(entites[0]).toBeInstanceOf(Ent); 308 | expect(entites.map(ent => ent.id)).toEqual(expect.arrayContaining([ 309 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 310 | 30, 40, 41, 42, 43, 44, 45, 49 311 | ])); 312 | await monitor.wait(100); 313 | // req[0] is sscan 314 | // req[1] is a:mySet 315 | expect(monitor.requests.map(req => [req[0], [req[1]]]).filter(([name]) => name === "sscan").length).toBeGreaterThan(4); 316 | await monitor.clearMonitorCalls(); 317 | 318 | for await (const val of set.values(150)) { 319 | // tslint:disable-next-line:no-unused-expression 320 | val; 321 | } 322 | await monitor.wait(100); 323 | expect(monitor.requests.map(req => [req[0], [req[1]]]).filter(([name]) => name === "sscan").length).toBe(1); 324 | }); 325 | }); -------------------------------------------------------------------------------- /src/Collections/__tests__/RedisLazyMap_Spec.ts: -------------------------------------------------------------------------------- 1 | import { Connection } from "../../Connection/Connection"; 2 | import { Entity } from "../../Decorators/Entity"; 3 | import { IdentifyProperty } from "../../Decorators/IdentifyProperty"; 4 | import { Property } from "../../Decorators/Property"; 5 | import { RedisManager } from "../../Persistence/RedisManager"; 6 | import { cleanRedisConnection, createRedisConnection } from "../../testutils/redis"; 7 | import { RedisTestMonitor } from "../../testutils/RedisTestMonitor"; 8 | import { RedisLazyMap } from "../RedisLazyMap"; 9 | 10 | let conn: Connection; 11 | let manager: RedisManager; 12 | let monitor: RedisTestMonitor; 13 | beforeAll(async () => { 14 | conn = await createRedisConnection(); 15 | manager = conn.manager; 16 | monitor = await RedisTestMonitor.create(conn); 17 | 18 | }); 19 | 20 | afterEach(async () => { 21 | await monitor.clearMonitorCalls(50); 22 | await conn.flushdb(); 23 | }); 24 | 25 | afterAll(async () => { 26 | await monitor.release(); 27 | await cleanRedisConnection(conn); 28 | }); 29 | 30 | describe("Size", () => { 31 | it("Returns size of map", async () => { 32 | await conn.client.hmsetAsync("myMap", { 33 | "i:1": "s:a", 34 | "i:2": "s:a", 35 | "i:3": "s:a", 36 | "i:4": "s:a", 37 | "i:5": "s:a", 38 | }); 39 | const map = new RedisLazyMap("myMap", manager); 40 | expect(await map.size()).toBe(5); 41 | }); 42 | }); 43 | 44 | describe("Set", () => { 45 | it("Sets simple values for map", async () => { 46 | const map = new RedisLazyMap("myMap", manager); 47 | await map.set(1, "test"); 48 | await map.set("1", 1); 49 | await map.set(2, true); 50 | await map.set(3, { a: true }); 51 | 52 | const res = await conn.client.hgetallAsync("myMap"); 53 | expect(res).toEqual({ 54 | "i:1": "s:test", 55 | "s:1": "i:1", 56 | "i:2": "b:1", 57 | "i:3": "j:" + JSON.stringify({ a: true }) 58 | }); 59 | }); 60 | 61 | it("Sets entities in map but doesn't save entities without cascade insert", async () => { 62 | @Entity() 63 | class Ent { 64 | @IdentifyProperty() 65 | public id: number; 66 | 67 | @Property() 68 | public prop: string = "prop"; 69 | } 70 | 71 | const map = new RedisLazyMap("myMap", manager, Ent, false); 72 | const ent1 = new Ent(); 73 | ent1.id = 1; 74 | const ent2 = new Ent(); 75 | ent2.id = 2; 76 | await map.set(1, ent1); 77 | await map.set(2, ent2); 78 | const mapRes = await conn.client.hgetallAsync("myMap"); 79 | expect(mapRes).toEqual({ 80 | "i:1": "e:Ent:1", 81 | "i:2": "e:Ent:2" 82 | }); 83 | await map.set(3, ent2); 84 | const exists = await Promise.all([ 85 | conn.client.existsAsync("e:Ent:1"), 86 | conn.client.existsAsync("e:Ent:2"), 87 | ]); 88 | expect(exists).toEqual([0, 0]); 89 | }); 90 | 91 | it("Sets entities in map with cascade insert", async () => { 92 | @Entity() 93 | class Ent { 94 | @IdentifyProperty() 95 | public id: number; 96 | 97 | @Property() 98 | public prop: string = "prop"; 99 | } 100 | 101 | const map = new RedisLazyMap("myMap", manager, Ent, true); 102 | const ent1 = new Ent(); 103 | ent1.id = 1; 104 | const ent2 = new Ent(); 105 | ent2.id = 2; 106 | await map.set(1, ent1); 107 | await map.set(2, ent2); 108 | await map.set(3, ent2); 109 | 110 | const mapRes = await conn.client.hgetallAsync("myMap"); 111 | expect(mapRes).toEqual({ 112 | "i:1": "e:Ent:1", 113 | "i:2": "e:Ent:2", 114 | "i:3": "e:Ent:2", 115 | }); 116 | let entRes = await conn.client.hgetallAsync("e:Ent:1"); 117 | expect(entRes).toEqual({ 118 | id: "i:1", 119 | prop: "s:prop" 120 | }); 121 | entRes = await conn.client.hgetallAsync("e:Ent:2"); 122 | expect(entRes).toEqual({ 123 | id: "i:2", 124 | prop: "s:prop" 125 | }); 126 | }); 127 | }); 128 | 129 | describe("Delete", () => { 130 | it("Deletes simple value by key", async () => { 131 | await conn.client.hmsetAsync("myMap", { 132 | "i:1": "s:a", 133 | "i:2": "s:a", 134 | "i:3": "s:a", 135 | "i:4": "s:a", 136 | "i:5": "s:a", 137 | }); 138 | const map = new RedisLazyMap("myMap", manager); 139 | await map.delete(5); 140 | await map.delete(3); 141 | 142 | const res = await conn.client.hgetallAsync("myMap"); 143 | expect(res).toEqual({ 144 | "i:1": "s:a", 145 | "i:2": "s:a", 146 | "i:4": "s:a", 147 | }); 148 | }); 149 | 150 | it("Deletes entity by key without deleting entities", async () => { 151 | @Entity() 152 | class Ent { 153 | @IdentifyProperty() 154 | public id: number; 155 | 156 | @Property() 157 | public prop: string = "prop"; 158 | } 159 | 160 | const map = new RedisLazyMap("myMap", manager, Ent, true); 161 | const ent1 = new Ent(); 162 | ent1.id = 1; 163 | const ent2 = new Ent(); 164 | ent2.id = 2; 165 | await map.set(1, ent1); 166 | await map.set(2, ent2); 167 | await map.set(3, ent2); 168 | 169 | await map.delete(3); 170 | await map.delete(1); 171 | const res = await conn.client.hgetallAsync("myMap"); 172 | expect(res).toEqual({ 173 | "i:2": "e:Ent:2", 174 | }); 175 | const exists = await Promise.all([ 176 | conn.client.existsAsync("e:Ent:1"), 177 | conn.client.existsAsync("e:Ent:2"), 178 | ]); 179 | expect(exists).toEqual([1, 1]); 180 | }); 181 | 182 | it("Deletes entity by key with deleting entity", async () => { 183 | @Entity() 184 | class Ent { 185 | @IdentifyProperty() 186 | public id: number; 187 | 188 | @Property() 189 | public prop: string = "prop"; 190 | } 191 | 192 | const map = new RedisLazyMap("myMap", manager, Ent, true); 193 | const ent1 = new Ent(); 194 | ent1.id = 1; 195 | const ent2 = new Ent(); 196 | ent2.id = 2; 197 | await map.set(1, ent1); 198 | await map.set(2, ent2); 199 | 200 | await map.delete(1, true); 201 | const res = await conn.client.hgetallAsync("myMap"); 202 | expect(res).toEqual({ 203 | "i:2": "e:Ent:2", 204 | }); 205 | const exists = await Promise.all([ 206 | conn.client.existsAsync("e:Ent:1"), 207 | conn.client.existsAsync("e:Ent:2"), 208 | ]); 209 | expect(exists).toEqual([0, 1]); 210 | }); 211 | }); 212 | 213 | describe("Has", () => { 214 | it("Checks by simple keys", async () => { 215 | await conn.client.hmsetAsync("myMap", { 216 | "i:1": "s:a", 217 | "s:test": "s:a", 218 | "i:3": "s:a", 219 | "i:4": "s:a", 220 | "s:5": "s:a", 221 | }); 222 | const map = new RedisLazyMap("myMap", manager); 223 | expect(await map.has(1)).toBeTruthy(); 224 | expect(await map.has(5)).toBeFalsy(); 225 | expect(await map.has("5")).toBeTruthy(); 226 | expect(await map.has("test")).toBeTruthy(); 227 | expect(await map.has(3)).toBeTruthy(); 228 | }); 229 | 230 | it("Checks with entities", async () => { 231 | @Entity() 232 | class A { 233 | @IdentifyProperty() 234 | public id: number; 235 | } 236 | await conn.client.hmsetAsync("myMap", { 237 | "i:1": "e:A:1", 238 | "i:2": "e:A:2", 239 | }); 240 | await conn.client.hmsetAsync("e:A:1", { 241 | id: "i:1" 242 | }); 243 | const map = new RedisLazyMap("myMap", manager, A); 244 | expect(await map.has(1)).toBeTruthy(); 245 | // entity doesn't exist, so false 246 | expect(await map.has(2)).toBeFalsy(); 247 | }); 248 | }); 249 | 250 | describe("Get", () => { 251 | it("Return simple values for simple map", async () => { 252 | await conn.client.hmsetAsync("myMap", { 253 | "i:1": "s:test", 254 | "s:test": "i:10", 255 | "i:3": "b:0", 256 | "s:5": "s:a", 257 | }); 258 | const map = new RedisLazyMap("myMap", manager); 259 | expect(await map.get(1)).toBe("test"); 260 | expect(await map.get("test")).toBe(10); 261 | expect(await map.get(3)).toBe(false); 262 | expect(await map.get("5")).toBe("a"); 263 | }); 264 | 265 | it("Returns entities for entity map", async () => { 266 | @Entity() 267 | class A { 268 | @IdentifyProperty() 269 | public id: number; 270 | } 271 | 272 | const map = new RedisLazyMap("myMap", manager, A); 273 | const a = new A(); 274 | a.id = 1; 275 | // not saved 276 | const a2 = new A(); 277 | a2.id = 2; 278 | await manager.save(a); 279 | 280 | await map.set(1, a); 281 | await map.set(2, a2); 282 | 283 | const ret1 = await map.get(1); 284 | expect(ret1).toBeInstanceOf(A); 285 | expect(ret1!.id).toBe(1); 286 | const ret2 = await map.get(2); 287 | expect(ret2).toBeUndefined(); 288 | }); 289 | }); 290 | 291 | describe("Clear", () => { 292 | it("removes map", async () => { 293 | await conn.client.hmsetAsync("myMap", { 294 | "i:1": "s:test", 295 | }); 296 | const map = new RedisLazyMap("myMap", manager); 297 | await map.clear(); 298 | const res = await conn.client.existsAsync("myMap"); 299 | expect(res).toBe(0); 300 | }); 301 | 302 | it("removes map and entities if specified", async () => { 303 | @Entity() 304 | class A { 305 | @IdentifyProperty() 306 | public id: number; 307 | } 308 | 309 | const map = new RedisLazyMap("myMap", manager, A); 310 | const a = new A(); 311 | a.id = 1; 312 | const a2 = new A(); 313 | a2.id = 2; 314 | await manager.save(a); 315 | await manager.save(a2); 316 | 317 | await map.set(1, a); 318 | await map.set(2, a2); 319 | 320 | await map.clear(); 321 | let res = await Promise.all([ 322 | conn.client.existsAsync("myMap"), 323 | conn.client.existsAsync("e:A:1"), 324 | conn.client.existsAsync("e:A:2"), 325 | ]); 326 | expect(res).toEqual([0, 1, 1]); 327 | 328 | await map.set(1, a); 329 | await map.set(2, a2); 330 | await map.clear(true); 331 | res = await Promise.all([ 332 | conn.client.existsAsync("myMap"), 333 | conn.client.existsAsync("e:A:1"), 334 | conn.client.existsAsync("e:A:2"), 335 | ]); 336 | expect(res).toEqual([0, 0, 0]); 337 | }); 338 | }); 339 | 340 | describe("toArray", () => { 341 | it("Returns array of map pairs", async () => { 342 | await conn.client.hmsetAsync("myMap", { 343 | "i:1": "s:a", 344 | "i:2": "s:a", 345 | "i:3": "s:a", 346 | "i:4": "s:a", 347 | "i:5": "s:a", 348 | }); 349 | const map = new RedisLazyMap("myMap", manager); 350 | expect(await map.toArray()).toEqual([ 351 | [1, "a"], 352 | [2, "a"], 353 | [3, "a"], 354 | [4, "a"], 355 | [5, "a"], 356 | ]); 357 | }); 358 | }); 359 | 360 | describe("Iterators", () => { 361 | it("Iterates over keys for simple map", async () => { 362 | await conn.client.hmsetAsync("myMap", { 363 | "i:1": "s:test", 364 | "s:test": "i:10", 365 | "i:3": "b:0", 366 | "s:5": "s:a", 367 | }); 368 | const map = new RedisLazyMap("myMap", manager); 369 | 370 | const keys: any[] = []; 371 | for await (const key of map.keys()) { 372 | keys.push(key); 373 | } 374 | expect(keys).toEqual([1, "test", 3, "5"]); 375 | }); 376 | 377 | it("Iterates over keys for entity map", async () => { 378 | @Entity() 379 | class A { 380 | @IdentifyProperty() 381 | public id: number; 382 | } 383 | const map = new RedisLazyMap("myMap", manager, A); 384 | const a1 = new A(); 385 | a1.id = 1; 386 | const a2 = new A(); 387 | a2.id = 1; 388 | await map.set(1, a1); 389 | await map.set("1", a2); 390 | 391 | const keys: any[] = []; 392 | for await (const key of map.keys()) { 393 | keys.push(key); 394 | } 395 | expect(keys).toEqual([1, "1"]); 396 | }); 397 | 398 | it("Iterates over keys/values for simple map", async () => { 399 | await conn.client.hmsetAsync("myMap", { 400 | "i:1": "s:test", 401 | "s:test": "i:10", 402 | "i:3": "b:0", 403 | "s:5": "s:a", 404 | }); 405 | const map = new RedisLazyMap("myMap", manager); 406 | const vals: any[] = []; 407 | for await (const val of map.values()) { 408 | vals.push(val); 409 | } 410 | expect(vals).toEqual(["test", 10, false, "a"]); 411 | 412 | const pairs: any[] = []; 413 | for await (const keyVal of map.keysAndValues()) { 414 | pairs.push(keyVal); 415 | } 416 | expect(pairs).toEqual([ 417 | [1, "test"], 418 | ["test", 10], 419 | [3, false], 420 | ["5", "a"] 421 | ]); 422 | }); 423 | 424 | it("Iterates over keys/values for entity map", async () => { 425 | @Entity() 426 | class A { 427 | @IdentifyProperty() 428 | public id: number; 429 | } 430 | const map = new RedisLazyMap("myMap", manager, A, true); 431 | const a1 = new A(); 432 | const a2 = new A(); 433 | const a3 = new A(); 434 | const a4 = new A(); 435 | a1.id = 1; 436 | a2.id = 2; 437 | a3.id = 3; 438 | a4.id = 4; 439 | await map.set(1, a1); 440 | await map.set(2, a3); 441 | await map.set(3, a2); 442 | await map.set(4, a3); 443 | await map.set(5, a4); 444 | await map.set(6, a4); 445 | 446 | const vals: A[] = []; 447 | for await (const val of map.values()) { 448 | vals.push(val); 449 | } 450 | expect(vals).toHaveLength(6); 451 | expect(vals.map(val => val.id)).toEqual([1, 3, 2, 3, 4, 4]); 452 | expect(vals[0]).toBeInstanceOf(A); 453 | 454 | const pairs: Array<[number, A]> = []; 455 | for await (const keyVal of map.keysAndValues()) { 456 | pairs.push(keyVal); 457 | } 458 | expect(pairs.map(pair => [pair[0], pair[1].id])).toEqual([ 459 | [1, 1], 460 | [2, 3], 461 | [3, 2], 462 | [4, 3], 463 | [5, 4], 464 | [6, 4] 465 | ]); 466 | }); 467 | 468 | it("Iterates with scan option", async () => { 469 | await conn.client.hmsetAsync("myMap", { 470 | "i:1": "s:test", 471 | "s:test": "i:10", 472 | "i:3": "b:0", 473 | "s:5": "s:a", 474 | "i:2": "s:11", 475 | "i:8": "s:12", 476 | "i:9": "s:13", 477 | "i:10": "s:14", 478 | "i:11": "s:15", 479 | "i:12": "s:15", 480 | "i:13": "s:15", 481 | "i:14": "s:15", 482 | "i:15": "s:15", 483 | "i:16": "s:15", 484 | "i:17": "s:15", 485 | "i:18": "s:15", 486 | "i:19": "s:15", 487 | "i:20": "s:15", 488 | "i:21": "b:1", 489 | "i:22": "i:0", 490 | "i:23": "i:5", 491 | }); 492 | const map = new RedisLazyMap("myMap", manager); 493 | await monitor.clearMonitorCalls(100); 494 | 495 | for await (const v of map.keys(50)) { 496 | // tslint:disable-next-line:no-unused-expression 497 | v; 498 | } 499 | await monitor.wait(100); 500 | expect(monitor.requests.length).toBe(1); 501 | }); 502 | }); -------------------------------------------------------------------------------- /src/Persistence/RedisManager.ts: -------------------------------------------------------------------------------- 1 | import { LazyMap } from "../Collections/LazyMap"; 2 | import { LazySet } from "../Collections/LazySet"; 3 | import { RedisLazyMap } from "../Collections/RedisLazyMap"; 4 | import { RedisLazySet } from "../Collections/RedisLazySet"; 5 | import { Connection } from "../Connection/Connection"; 6 | import { MetadataError } from "../Errors/Errors"; 7 | import { getEntityFullId, getEntityProperties, getRelationType, isRedisEntity } from "../Metadata/Metadata"; 8 | import { EntitySubscriberInterface } from "../Subscriber/EntitySubscriberInterface"; 9 | import { hasPrototypeOf } from "../utils/hasPrototypeOf"; 10 | import { HydrationData, LoadOperation, Operator, PersistenceOperation } from "./Operator"; 11 | 12 | export type EntityType = { new(): T } | Function; 13 | 14 | /** 15 | * Main manager to get/save/remove entities 16 | * 17 | * @export 18 | * @class RedisManager 19 | */ 20 | export class RedisManager { 21 | /** 22 | * Connection instance 23 | * 24 | * @protected 25 | */ 26 | public connection: Connection; 27 | 28 | /** 29 | * Array of entity subscribers 30 | * 31 | * @protected 32 | */ 33 | protected subscribers: Array> = []; 34 | 35 | /** 36 | * Entity operator 37 | * 38 | * @protected 39 | */ 40 | protected operator: Operator; 41 | 42 | /** 43 | * Creates an instance of RedisManager. 44 | * @param connection 45 | */ 46 | public constructor(connection: Connection) { 47 | this.connection = connection; 48 | this.operator = new Operator(); 49 | } 50 | 51 | /** 52 | * Assign entity subscribers 53 | * 54 | * @param subscribers 55 | */ 56 | public assignSubscribers(subscribers: Array>) { 57 | this.subscribers = subscribers; 58 | } 59 | 60 | /** 61 | * Save entity 62 | * 63 | * @template T 64 | * @param entity 65 | * @returns 66 | */ 67 | public async save(entity: T): Promise { 68 | // Run beforeSave subscribers for all entities regardless will they be finally saved or no 69 | // started from most deep relation entity to root entity 70 | const allEntities = this.getEntitiesForSubscribers(entity).reverse(); 71 | for (const ent of allEntities) { 72 | const subscriber = this.subscribers.find(subscriber => subscriber.listenTo() === ent.constructor); 73 | if (subscriber && subscriber.beforeSave) { 74 | subscriber.beforeSave(ent); 75 | } 76 | } 77 | 78 | const operation = await this.operator.getSaveOperation(entity); 79 | if (this.isEmptyPersistenceOperation(operation)) { 80 | return; 81 | } 82 | 83 | // Call entities subscribers 84 | // Do operation 85 | await this.connection.batch(executor => { 86 | for (const deleteSet of operation.deletesSets) { 87 | executor.del(deleteSet); 88 | } 89 | for (const deleteHash of operation.deleteHashes) { 90 | executor.del(deleteHash); 91 | } 92 | for (const modifySet of operation.modifySets) { 93 | if (modifySet.removeValues.length > 0) { 94 | executor.srem(modifySet.setName, modifySet.removeValues); 95 | } 96 | if (modifySet.addValues.length > 0) { 97 | executor.sadd(modifySet.setName, modifySet.addValues); 98 | } 99 | } 100 | for (const modifyHash of operation.modifyHashes) { 101 | if (modifyHash.deleteKeys.length > 0) { 102 | executor.hdel(modifyHash.hashId, modifyHash.deleteKeys); 103 | } 104 | if (Object.keys(modifyHash.changeKeys).length > 0) { 105 | executor.hmset(modifyHash.hashId, modifyHash.changeKeys); 106 | } 107 | } 108 | }); 109 | // update metadata 110 | this.operator.updateMetadataInHash(entity); 111 | this.initLazyCollections(entity); 112 | // Call entities subscribers 113 | for (const ent of this.filterEntitiesForPersistenceOperation(allEntities, operation)) { 114 | const subscriber = this.subscribers.find(subscriber => subscriber.listenTo() === ent.constructor); 115 | if (subscriber && subscriber.afterSave) { 116 | subscriber.afterSave(ent); 117 | } 118 | } 119 | } 120 | 121 | /** 122 | * Check if there is a given entity with given id 123 | * 124 | * @param entityClass 125 | * @param id 126 | * @returns 127 | */ 128 | public async has(entityClass: EntityType, id: string | number): Promise { 129 | const fullId = getEntityFullId(entityClass, id); 130 | if (!fullId) { 131 | throw new MetadataError(entityClass, "Unable to get entity id"); 132 | } 133 | const exists = await this.connection.client.existsAsync(fullId); 134 | return exists === 1 ? true : false; 135 | } 136 | 137 | 138 | /** 139 | * Get entity 140 | * 141 | * @template T 142 | * @param entityClass 143 | * @param id 144 | * @param [skipRelations] 145 | * @returns 146 | */ 147 | public async load(entityClass: EntityType, id: string | number, skipRelations?: Array): Promise; 148 | /** 149 | * Get entities 150 | * 151 | * @template T 152 | * @param entityClass 153 | * @param id 154 | * @param [skipRelations] 155 | * @returns 156 | */ 157 | public async load(entityClass: EntityType, id: string[] | number[], skipRelations?: Array): Promise; 158 | public async load(entityClass: EntityType, id: string | string[] | number | number[], skipRelations?: Array): Promise { 159 | const idsToLoad = Array.isArray(id) ? id : [id]; 160 | 161 | const entityIdToClassMap: Map> = new Map(); 162 | const loadedData: Map = new Map(); 163 | const rootLoadOperations = idsToLoad.map(id => this.operator.getLoadOperation(id, entityClass, skipRelations)).filter(op => !!op) as LoadOperation[]; 164 | 165 | rootLoadOperations.forEach(op => entityIdToClassMap.set(op.entityId, entityClass)); 166 | 167 | const recursiveLoadDataWithRelations = async (operations: LoadOperation[]) => { 168 | const loadedDataForCall: HydrationData[] = []; 169 | 170 | // easier to use callbacks instead of processing result 171 | await this.connection.batch(executor => { 172 | for (const op of operations) { 173 | executor.hgetall(op.entityId, (err, result) => 174 | loadedDataForCall.push({ id: op.entityId, redisData: result, entityClass: entityIdToClassMap.get(op.entityId) })); 175 | for (const hash of op.hashes) { 176 | executor.hgetall(hash, (err, result) => loadedDataForCall.push({ id: hash, redisData: result })); 177 | } 178 | for (const set of op.sets) { 179 | executor.smembers(set, (err, result) => loadedDataForCall.push({ id: set, redisData: result })); 180 | } 181 | } 182 | }); 183 | loadedDataForCall.forEach(data => loadedData.set(data.id, data)); 184 | const allMappings: LoadOperation["relationMappings"] = []; 185 | for (const op of operations) { 186 | allMappings.push(...op.relationMappings); 187 | } 188 | 189 | const relationOperations: Array = []; 190 | for (const mapping of allMappings) { 191 | const relationClass = mapping.relationClass; 192 | switch (mapping.type) { 193 | // single relation in key 194 | case "key": { 195 | const hashVal = loadedDataForCall.find(data => data.id === mapping.ownerId); 196 | if (hashVal && hashVal.redisData && !Array.isArray(hashVal.redisData) && hashVal.redisData[mapping.id]) { 197 | const relationVal = hashVal.redisData[mapping.id]; 198 | if (relationVal && !loadedData.has(relationVal)) { 199 | entityIdToClassMap.set(relationVal, relationClass); 200 | relationOperations.push(this.operator.getLoadOperation(relationVal, relationClass)); 201 | } 202 | } 203 | break; 204 | } 205 | // set of relations 206 | case "set": { 207 | const set = loadedDataForCall.find(data => data.id === mapping.id); 208 | if (set && set.redisData && Array.isArray(set.redisData)) { 209 | for (const setVal of set.redisData) { 210 | if (!loadedData.has(setVal)) { 211 | entityIdToClassMap.set(setVal, relationClass); 212 | relationOperations.push(this.operator.getLoadOperation(setVal, relationClass)); 213 | } 214 | } 215 | } 216 | break; 217 | } 218 | // map of relations 219 | case "map": { 220 | const map = loadedDataForCall.find(data => data.id === mapping.id); 221 | if (map && map.redisData && !Array.isArray(map.redisData) && Object.keys(map.redisData).length > 0) { 222 | for (const key of Object.keys(map.redisData)) { 223 | const relVal = map.redisData[key]; 224 | if (!loadedData.has(relVal)) { 225 | entityIdToClassMap.set(relVal, relationClass); 226 | relationOperations.push(this.operator.getLoadOperation(relVal, relationClass)); 227 | } 228 | } 229 | } 230 | break; 231 | } 232 | } 233 | } 234 | if (relationOperations.length > 0) { 235 | await recursiveLoadDataWithRelations(relationOperations.filter(op => !!op) as LoadOperation[]); 236 | } 237 | }; 238 | await recursiveLoadDataWithRelations(rootLoadOperations); 239 | const hydratedData = this.operator.hydrateData(loadedData); 240 | // Init lazy collections 241 | hydratedData.forEach(data => { 242 | if (data && data.constructor && isRedisEntity(data)) { 243 | this.initLazyCollections(data); 244 | } 245 | }); 246 | // run subscribers in reverse order 247 | hydratedData.reduceRight((unusued, data) => { 248 | if (data && data.constructor) { 249 | const subscriber = this.subscribers.find(sub => sub.listenTo() === data.constructor); 250 | if (subscriber && subscriber.afterLoad) { 251 | subscriber.afterLoad(data); 252 | } 253 | } 254 | }, {}); 255 | 256 | return Array.isArray(id) 257 | ? hydratedData.filter(data => data && data.constructor === entityClass) 258 | : hydratedData.find(data => data && data.constructor === entityClass); 259 | } 260 | 261 | /** 262 | * Remove entity. Doesn't remove linked relations 263 | * 264 | * @param entity 265 | * @returns 266 | */ 267 | public async remove(entity: object): Promise { 268 | const operation = this.operator.getDeleteOperation(entity); 269 | if (this.isEmptyPersistenceOperation(operation)) { 270 | return; 271 | } 272 | // Since we don't support cascade delete no need to process relations for subscribers 273 | const subscriber = this.subscribers.find(sub => sub.listenTo() === entity.constructor); 274 | if (subscriber && subscriber.beforeRemove) { 275 | subscriber.beforeRemove(entity); 276 | } 277 | await this.connection.batch(executor => { 278 | for (const deleteSet of operation.deletesSets) { 279 | executor.del(deleteSet); 280 | } 281 | for (const deleteHash of operation.deleteHashes) { 282 | executor.del(deleteHash); 283 | } 284 | }); 285 | this.operator.resetMetadataInEntityObject(entity); 286 | if (subscriber && subscriber.afterRemove) { 287 | subscriber.afterRemove(entity); 288 | } 289 | } 290 | 291 | /** 292 | * Remove entity be it's id. This WON'T trigger entity subscribers beforeRemove/afterRemove 293 | * 294 | * @param entityClass 295 | * @param id 296 | * @returns 297 | */ 298 | public async removeById(entityClass: EntityType, id: string | number | string[] | number[]): Promise { 299 | const idsToRemove = Array.isArray(id) ? id : [id]; 300 | const operations = idsToRemove.map(id => this.operator.getDeleteOperation(entityClass, id)); 301 | if (operations.every(this.isEmptyPersistenceOperation)) { 302 | return; 303 | } 304 | await this.connection.batch(executor => { 305 | for (const operation of operations) { 306 | for (const deleteSet of operation.deletesSets) { 307 | executor.del(deleteSet); 308 | } 309 | for (const deleteHash of operation.deleteHashes) { 310 | executor.del(deleteHash); 311 | } 312 | } 313 | }); 314 | } 315 | 316 | /** 317 | * Serialize simple value to store it in redis 318 | * 319 | * @param value 320 | * @returns 321 | */ 322 | public serializeSimpleValue(value: any): string | undefined { 323 | return this.operator.serializeValue(value); 324 | } 325 | 326 | /** 327 | * Unserialize value 328 | * 329 | * @param value 330 | * @returns 331 | */ 332 | public unserializeSimpleValue(value: string): any { 333 | return this.operator.unserializeValue(value); 334 | } 335 | 336 | /** 337 | * Init lazy set and maps. This will replace LazySet/LazyMap instances with redis-backed analogs 338 | * 339 | * @param entity 340 | */ 341 | public initLazyCollections(entity: { [key: string]: any }): void { 342 | const id = getEntityFullId(entity); 343 | if (!id) { 344 | throw new MetadataError(entity.constructor, "Unable to get entity id"); 345 | } 346 | const properties = getEntityProperties(entity); 347 | if (!properties) { 348 | throw new MetadataError(entity.constructor, "No any properties"); 349 | } 350 | for (const prop of properties) { 351 | if (hasPrototypeOf(prop.propertyType, LazyMap) && !(entity[prop.propertyName] instanceof RedisLazyMap)) { 352 | const mapId = this.operator.getCollectionId(id, prop); 353 | if (mapId) { 354 | entity[prop.propertyName] = new RedisLazyMap( 355 | mapId, 356 | this, 357 | prop.isRelation 358 | ? getRelationType(entity, prop) 359 | : undefined, 360 | prop.isRelation 361 | ? prop.relationOptions.cascadeInsert 362 | : false 363 | ); 364 | } 365 | } else if (hasPrototypeOf(prop.propertyType, LazySet) && !(entity[prop.propertyName] instanceof RedisLazySet)) { 366 | const setId = this.operator.getCollectionId(id, prop); 367 | if (setId) { 368 | entity[prop.propertyName] = new RedisLazySet( 369 | setId, 370 | this, 371 | prop.isRelation 372 | ? getRelationType(entity, prop) 373 | : undefined, 374 | prop.isRelation 375 | ? prop.relationOptions.cascadeInsert 376 | : false 377 | ); 378 | } 379 | } 380 | } 381 | } 382 | 383 | 384 | /** 385 | * Return all entities which should fire related subscribers for save/update/delete operation 386 | * 387 | * @private 388 | * @param entity 389 | * @param operation 390 | * @param [entities=[]] 391 | * @returns 392 | */ 393 | private getEntitiesForSubscribers(entity: { [key: string]: any }, entities: object[] = []): object[] { 394 | if (!isRedisEntity(entity) || entities.includes(entity)) { 395 | return entities; 396 | } 397 | entities.push(entity); 398 | const metadata = getEntityProperties(entity); 399 | if (!metadata) { 400 | return entities; 401 | } 402 | // const redisHash = getRedisHashId(entity); 403 | for (const propMetadata of metadata) { 404 | // no need to process non relations or without cascading options 405 | if (!propMetadata.isRelation || !(propMetadata.relationOptions.cascadeInsert || propMetadata.relationOptions.cascadeUpdate)) { 406 | continue; 407 | } 408 | const propValue = entity[propMetadata.propertyName]; 409 | if (!propValue) { 410 | continue; 411 | } 412 | if (propValue instanceof Map || propValue instanceof Set) { 413 | for (const collVal of propValue.values()) { 414 | this.getEntitiesForSubscribers(collVal, entities); 415 | } 416 | } else { 417 | this.getEntitiesForSubscribers(propValue, entities); 418 | } 419 | } 420 | return entities; 421 | } 422 | 423 | /** 424 | * Return entities which were somehow changed with given operation 425 | * 426 | * @private 427 | * @param entities 428 | * @param operation 429 | * @returns 430 | */ 431 | private filterEntitiesForPersistenceOperation(entities: object[], operation: PersistenceOperation): object[] { 432 | return entities.filter(entity => { 433 | const hashId = getEntityFullId(entity); 434 | if (!hashId) { 435 | return false; 436 | } 437 | if (operation.deleteHashes.find(name => name.includes(hashId))) { 438 | return true; 439 | } 440 | if (operation.deletesSets.find(name => name.includes(hashId))) { 441 | return true; 442 | } 443 | if (operation.modifyHashes.find(v => ((v.deleteKeys.length > 0 || Object.keys(v.changeKeys).length > 0) && v.hashId.includes(hashId)))) { 444 | return true; 445 | } 446 | if (operation.modifySets.find(v => ((v.addValues.length > 0 || v.removeValues.length > 0) && v.setName.includes(hashId)))) { 447 | return true; 448 | } 449 | return false; 450 | }); 451 | } 452 | 453 | /** 454 | * True true if persistence operation is empty (i.e. doesn't have any changes) 455 | * 456 | * @private 457 | * @param operation 458 | * @returns 459 | */ 460 | private isEmptyPersistenceOperation(operation: PersistenceOperation): boolean { 461 | return operation.deleteHashes.length === 0 && operation.deletesSets.length === 0 && 462 | (operation.modifyHashes.length === 0 || operation.modifyHashes.every(val => val.deleteKeys.length === 0 && Object.keys(val.changeKeys).length === 0)) && 463 | (operation.modifySets.length === 0 || operation.modifySets.every(val => val.addValues.length === 0 && val.removeValues.length === 0)); 464 | } 465 | } -------------------------------------------------------------------------------- /src/utils/PromisedRedis.ts: -------------------------------------------------------------------------------- 1 | import { RedisClient, ServerInfo } from "redis"; 2 | import { promisifyAll } from "sb-promisify"; 3 | 4 | export interface PromisedOverloadedCommand { 5 | (args: T[]): Promise; 6 | (arg: T, args: T[]): Promise; 7 | 8 | (arg1: T, arg2: T, arg3: T, arg4: T, arg5: T, arg6: T): Promise; 9 | (arg1: T, arg2: T, arg3: T, arg4: T, arg5: T, ): Promise; 10 | (arg1: T, arg2: T, arg3: T, arg4: T): Promise; 11 | (arg1: T, arg2: T, arg3: T): Promise; 12 | (arg1: T, arg2: T): Promise; 13 | (arg1: T): Promise; 14 | (...args: T[]): Promise; 15 | } 16 | 17 | export interface PromisedOverloadedKeyCommand { 18 | (key: string, args: T[]): Promise; 19 | 20 | (key: string, arg1: T, arg2: T, arg3: T, arg4: T, arg5: T, arg6: T): Promise; 21 | (key: string, arg1: T, arg2: T, arg3: T, arg4: T, arg5: T): Promise; 22 | (key: string, arg1: T, arg2: T, arg3: T, arg4: T): Promise; 23 | (key: string, arg1: T, arg2: T, arg3: T): Promise; 24 | (key: string, arg1: T, arg2: T): Promise; 25 | (key: string, arg1: T): Promise; 26 | (key: string, ...args: T[]): Promise; 27 | } 28 | 29 | export interface PromisedOverloadedListCommand { 30 | (args: T[]): Promise; 31 | 32 | (arg1: T, arg2: T, arg3: T, arg4: T, arg5: T, arg6: T): Promise; 33 | (arg1: T, arg2: T, arg3: T, arg4: T, arg5: T): Promise; 34 | (arg1: T, arg2: T, arg3: T, arg4: T): Promise; 35 | (arg1: T, arg2: T, arg3: T): Promise; 36 | (arg1: T, arg2: T): Promise; 37 | (arg1: T): Promise; 38 | (...args: T[]): Promise; 39 | } 40 | 41 | export interface PromisedOverloadedSetCommand { 42 | (key: string, args: { [key: string]: T } | T[]): Promise; 43 | 44 | (key: string, arg1: T, arg2: T, arg3: T, arg4: T, arg5: T, arg6: T): Promise; 45 | (key: string, arg1: T, arg2: T, arg3: T, arg4: T, arg5: T): Promise; 46 | (key: string, arg1: T, arg2: T, arg3: T, arg4: T): Promise; 47 | (key: string, arg1: T, arg2: T, arg3: T): Promise; 48 | (key: string, arg1: T, arg2: T): Promise; 49 | (key: string, arg1: T): Promise; 50 | (key: string, ...args: T[]): Promise; 51 | } 52 | 53 | export interface PromisedOverloadedLastCommand { 54 | (args: Array): Promise; 55 | (arg: T1, args: Array): Promise; 56 | 57 | (arg1: T1, arg2: T1, arg3: T1, arg4: T1, arg5: T1, arg6: T2): Promise; 58 | (arg1: T1, arg2: T1, arg3: T1, arg4: T1, arg5: T2): Promise; 59 | (arg1: T1, arg2: T1, arg3: T1, arg4: T2): Promise; 60 | (arg1: T1, arg2: T1, arg3: T2): Promise; 61 | (arg1: T1, arg2: T2): Promise; 62 | (...args: Array): Promise; 63 | } 64 | 65 | // tslint:disable:member-ordering 66 | export interface PromisedCommands { 67 | /** 68 | * Listen for all requests received by the server in real time. 69 | */ 70 | monitorAsync(): Promise; 71 | 72 | /** 73 | * Get information and statistics about the server. 74 | */ 75 | infoAsync(): Promise; 76 | infoAsync(section?: string | string[]): Promise; 77 | 78 | /** 79 | * Ping the server. 80 | */ 81 | pingAsync(message: string): Promise; 82 | 83 | /** 84 | * Post a message to a channel. 85 | */ 86 | publishAsync(channel: string, value: string): Promise; 87 | 88 | /** 89 | * Authenticate to the server. 90 | */ 91 | authAsync(password: string): Promise; 92 | 93 | /** 94 | * KILL - Kill the connection of a client. 95 | * LIST - Get the list of client connections. 96 | * GETNAME - Get the current connection name. 97 | * PAUSE - Stop processing commands from clients for some time. 98 | * REPLY - Instruct the server whether to reply to commands. 99 | * SETNAME - Set the current connection name. 100 | */ 101 | clientAsync: PromisedOverloadedCommand; 102 | 103 | 104 | /** 105 | * Set multiple hash fields to multiple values. 106 | */ 107 | hmsetAsync: PromisedOverloadedSetCommand; 108 | 109 | /** 110 | * Listen for messages published to the given channels. 111 | */ 112 | subscribeAsync: PromisedOverloadedListCommand; 113 | 114 | /** 115 | * Stop listening for messages posted to the given channels. 116 | */ 117 | unsubscribeAsync: PromisedOverloadedListCommand; 118 | 119 | /** 120 | * Listen for messages published to channels matching the given patterns. 121 | */ 122 | psubscribeAsync: PromisedOverloadedListCommand; 123 | 124 | /** 125 | * Stop listening for messages posted to channels matching the given patterns. 126 | */ 127 | punsubscribeAsync: PromisedOverloadedListCommand; 128 | 129 | /** 130 | * Append a value to a key. 131 | */ 132 | appendAsync(key: string, value: string): Promise; 133 | 134 | /** 135 | * Asynchronously rewrite the append-only file. 136 | */ 137 | bgrewriteaofAsync(): Promise<"OK">; 138 | 139 | /** 140 | * Asynchronously save the dataset to disk. 141 | */ 142 | bgsaveAsync(): Promise; 143 | 144 | /** 145 | * Count set bits in a string. 146 | */ 147 | bitcountAsync(key: string): Promise; 148 | bitcountAsync(key: string, start: number, end: number): Promise; 149 | 150 | /** 151 | * Perform arbitrary bitfield integer operations on strings. 152 | */ 153 | bitfieldAsync: PromisedOverloadedKeyCommand; 154 | 155 | /** 156 | * Perform bitwise operations between strings. 157 | */ 158 | bitopAsync(operation: string, destkey: string, key1: string, key2: string, key3: string): Promise; 159 | bitopAsync(operation: string, destkey: string, key1: string, key2: string): Promise; 160 | bitopAsync(operation: string, destkey: string, key: string): Promise; 161 | bitopAsync(operation: string, destkey: string, ...args: string[]): Promise; 162 | 163 | /** 164 | * Find first bit set or clear in a string. 165 | */ 166 | bitposAsync(key: string, bit: number, start: number, end: number): Promise; 167 | bitposAsync(key: string, bit: number, start: number): Promise; 168 | bitposAsync(key: string, bit: number): Promise; 169 | 170 | /** 171 | * Remove and get the first element in a list, or block until one is available. 172 | */ 173 | blpopAsync: PromisedOverloadedLastCommand; 174 | 175 | /** 176 | * Remove and get the last element in a list, or block until one is available. 177 | */ 178 | brpopAsync: PromisedOverloadedLastCommand; 179 | 180 | /** 181 | * Pop a value from a list, push it to another list and return it; or block until one is available. 182 | */ 183 | brpoplpushAsync(source: string, destination: string, timeout: number): Promise<[string, string]>; 184 | 185 | /** 186 | * ADDSLOTS - Assign new hash slots to receiving node. 187 | * COUNT-FAILURE-REPORTS - Return the number of failure reports active for a given node. 188 | * COUNTKEYSINSLOT - Return the number of local keys in the specified hash slot. 189 | * DELSLOTS - Set hash slots as unbound in receiving node. 190 | * FAILOVER - Forces a slave to perform a manual failover of its master. 191 | * FORGET - Remove a node from the nodes table. 192 | * GETKEYSINSLOT - Return local key names in the specified hash slot. 193 | * INFO - Provides info about Redis Cluster node state. 194 | * KEYSLOT - Returns the hash slot of the specified key. 195 | * MEET - Force a node cluster to handshake with another node. 196 | * NODES - Get cluster config for the node. 197 | * REPLICATE - Reconfigure a node as a slave of the specified master node. 198 | * RESET - Reset a Redis Cluster node. 199 | * SAVECONFIG - Forces the node to save cluster state on disk. 200 | * SET-CONFIG-EPOCH - Set the configuration epoch in a new node. 201 | * SETSLOT - Bind a hash slot to a specified node. 202 | * SLAVES - List slave nodes of the specified master node. 203 | * SLOTS - Get array of Cluster slot to node mappings. 204 | */ 205 | clusterAsync: PromisedOverloadedCommand; 206 | 207 | /** 208 | * Get array of Redis command details. 209 | * 210 | * COUNT - Get total number of Redis commands. 211 | * GETKEYS - Extract keys given a full Redis command. 212 | * INFO - Get array of specific REdis command details. 213 | */ 214 | commandAsync(): Promise>; 215 | 216 | /** 217 | * Get array of Redis command details. 218 | * 219 | * COUNT - Get array of Redis command details. 220 | * GETKEYS - Extract keys given a full Redis command. 221 | * INFO - Get array of specific Redis command details. 222 | * GET - Get the value of a configuration parameter. 223 | * REWRITE - Rewrite the configuration file with the in memory configuration. 224 | * SET - Set a configuration parameter to the given value. 225 | * RESETSTAT - Reset the stats returned by INFO. 226 | */ 227 | configAsync: PromisedOverloadedCommand; 228 | 229 | /** 230 | * Return the number of keys in the selected database. 231 | */ 232 | dbsizeAsync(): Promise; 233 | 234 | /** 235 | * OBJECT - Get debugging information about a key. 236 | * SEGFAULT - Make the server crash. 237 | */ 238 | debugAsync: PromisedOverloadedCommand; 239 | 240 | /** 241 | * Decrement the integer value of a key by one. 242 | */ 243 | decrAsync(key: string): Promise; 244 | 245 | /** 246 | * Decrement the integer value of a key by the given number. 247 | */ 248 | decrbyAsync(key: string, decrement: number): Promise; 249 | 250 | /** 251 | * Delete a key. 252 | */ 253 | delAsync: PromisedOverloadedCommand; 254 | 255 | /** 256 | * Discard all commands issued after MULTI. 257 | */ 258 | discardAsync(): Promise<"OK">; 259 | 260 | /** 261 | * Return a serialized version of the value stored at the specified key. 262 | */ 263 | dumpAsync(key: string): Promise; 264 | 265 | /** 266 | * Echo the given string. 267 | */ 268 | echoAsync(message: T): Promise; 269 | 270 | /** 271 | * Execute a Lua script server side. 272 | */ 273 | evalAsync: PromisedOverloadedCommand; 274 | 275 | /** 276 | * Execute a Lue script server side. 277 | */ 278 | evalshaAsync: PromisedOverloadedCommand; 279 | 280 | /** 281 | * Determine if a key exists. 282 | */ 283 | existsAsync: PromisedOverloadedCommand; 284 | 285 | /** 286 | * Set a key's time to live in seconds. 287 | */ 288 | expireAsync(key: string, seconds: number): Promise; 289 | 290 | /** 291 | * Set the expiration for a key as a UNIX timestamp. 292 | */ 293 | expireatAsync(key: string, timestamp: number): Promise; 294 | 295 | /** 296 | * Remove all keys from all databases. 297 | */ 298 | flushallAsync(): Promise; 299 | 300 | /** 301 | * Remove all keys from the current database. 302 | */ 303 | flushdbAsync(): Promise; 304 | 305 | /** 306 | * Add one or more geospatial items in the geospatial index represented using a sorted set. 307 | */ 308 | geoaddAsync: PromisedOverloadedKeyCommand; 309 | 310 | /** 311 | * Returns members of a geospatial index as standard geohash strings. 312 | */ 313 | geohashAsync: PromisedOverloadedKeyCommand; 314 | 315 | /** 316 | * Returns longitude and latitude of members of a geospatial index. 317 | */ 318 | geoposAsync: PromisedOverloadedKeyCommand>; 319 | 320 | /** 321 | * Returns the distance between two members of a geospatial index. 322 | */ 323 | geodistAsync: PromisedOverloadedKeyCommand; 324 | 325 | /** 326 | * Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a point. 327 | */ 328 | georadiusAsync: PromisedOverloadedKeyCommand>; 329 | 330 | /** 331 | * Query a sorted set representing a geospatial index to fetch members matching a given maximum distance from a member. 332 | */ 333 | georadiusbymemberAsync: PromisedOverloadedKeyCommand>; 334 | 335 | /** 336 | * Get the value of a key. 337 | */ 338 | getAsync(key: string): Promise; 339 | 340 | /** 341 | * Returns the bit value at offset in the string value stored at key. 342 | */ 343 | getbitAsync(key: string, offset: number): Promise; 344 | 345 | /** 346 | * Get a substring of the string stored at a key. 347 | */ 348 | getrangeAsync(key: string, start: number, end: number): Promise; 349 | 350 | /** 351 | * Set the string value of a key and return its old value. 352 | */ 353 | getsetAsync(key: string, value: string): Promise; 354 | 355 | /** 356 | * Delete on or more hash fields. 357 | */ 358 | hdelAsync: PromisedOverloadedKeyCommand; 359 | 360 | /** 361 | * Determine if a hash field exists. 362 | */ 363 | hexistsAsync(key: string, field: string): Promise; 364 | 365 | /** 366 | * Get the value of a hash field. 367 | */ 368 | hgetAsync(key: string, field: string): Promise; 369 | 370 | /** 371 | * Get all fields and values in a hash. 372 | */ 373 | hgetallAsync(key: string): Promise<{ [key: string]: string }>; 374 | 375 | /** 376 | * Increment the integer value of a hash field by the given number. 377 | */ 378 | hincrbyAsync(key: string, field: string, increment: number): Promise; 379 | 380 | /** 381 | * Increment the float value of a hash field by the given amount. 382 | */ 383 | hincrbyfloatAsync(key: string, field: string, increment: number): Promise; 384 | 385 | /** 386 | * Get all the fields of a hash. 387 | */ 388 | hkeysAsync(key: string): Promise; 389 | 390 | /** 391 | * Get the number of fields in a hash. 392 | */ 393 | hlenAsync(key: string): Promise; 394 | 395 | /** 396 | * Get the values of all the given hash fields. 397 | */ 398 | hmgetAsync: PromisedOverloadedKeyCommand; 399 | 400 | /** 401 | * Set the string value of a hash field. 402 | */ 403 | hsetAsync(key: string, field: string, value: string): Promise; 404 | 405 | /** 406 | * Set the value of a hash field, only if the field does not exist. 407 | */ 408 | hsetnxAsync(key: string, field: string, value: string): Promise; 409 | 410 | /** 411 | * Get the length of the value of a hash field. 412 | */ 413 | hstrlenAsync(key: string, field: string): Promise; 414 | 415 | /** 416 | * Get all the values of a hash. 417 | */ 418 | hvalsAsync(key: string): Promise; 419 | 420 | /** 421 | * Increment the integer value of a key by one. 422 | */ 423 | incrAsync(key: string): Promise; 424 | 425 | /** 426 | * Increment the integer value of a key by the given amount. 427 | */ 428 | incrbyAsync(key: string, increment: number): Promise; 429 | 430 | /** 431 | * Increment the float value of a key by the given amount. 432 | */ 433 | incrbyfloatAsync(key: string, increment: number): Promise; 434 | 435 | /** 436 | * Find all keys matching the given pattern. 437 | */ 438 | keysAsync(pattern: string): Promise; 439 | 440 | /** 441 | * Get the UNIX time stamp of the last successful save to disk. 442 | */ 443 | lastsaveAsync(): Promise; 444 | 445 | /** 446 | * Get an element from a list by its index. 447 | */ 448 | lindexAsync(key: string, index: number): Promise; 449 | 450 | /** 451 | * Insert an element before or after another element in a list. 452 | */ 453 | linsertAsync(key: string, dir: "BEFORE" | "AFTER", pivot: string, value: string): Promise; 454 | 455 | /** 456 | * Get the length of a list. 457 | */ 458 | llenAsync(key: string): Promise; 459 | 460 | /** 461 | * Remove and get the first element in a list. 462 | */ 463 | lpopAsync(key: string): Promise; 464 | 465 | /** 466 | * Prepend one or multiple values to a list. 467 | */ 468 | lpushAsync: PromisedOverloadedKeyCommand; 469 | 470 | /** 471 | * Prepend a value to a list, only if the list exists. 472 | */ 473 | lpushxAsync(key: string, value: string): Promise; 474 | 475 | /** 476 | * Get a range of elements from a list. 477 | */ 478 | lrangeAsync(key: string, start: number, stop: number): Promise; 479 | 480 | /** 481 | * Remove elements from a list. 482 | */ 483 | lremAsync(key: string, count: number, value: string): Promise; 484 | 485 | /** 486 | * Set the value of an element in a list by its index. 487 | */ 488 | lsetAsync(key: string, index: number, value: string): Promise<"OK">; 489 | 490 | /** 491 | * Trim a list to the specified range. 492 | */ 493 | ltrimAsync(key: string, start: number, stop: number): Promise<"OK">; 494 | 495 | /** 496 | * Get the values of all given keys. 497 | */ 498 | mgetAsync: PromisedOverloadedCommand; 499 | 500 | /** 501 | * Atomically tranfer a key from a Redis instance to another one. 502 | */ 503 | migrateAsync: PromisedOverloadedCommand; 504 | 505 | /** 506 | * Move a key to another database. 507 | */ 508 | moveAsync(key: string, db: string | number): void; 509 | 510 | /** 511 | * Set multiple keys to multiple values. 512 | */ 513 | msetAsync: PromisedOverloadedCommand; 514 | 515 | /** 516 | * Set multiple keys to multiple values, only if none of the keys exist. 517 | */ 518 | msetnxAsync: PromisedOverloadedCommand; 519 | 520 | /** 521 | * Inspect the internals of Redis objects. 522 | */ 523 | objectAsync: PromisedOverloadedCommand; 524 | 525 | /** 526 | * Remove the expiration from a key. 527 | */ 528 | persistAsync(key: string): Promise; 529 | 530 | /** 531 | * Remove a key's time to live in milliseconds. 532 | */ 533 | pexpireAsync(key: string, milliseconds: number): Promise; 534 | 535 | /** 536 | * Set the expiration for a key as a UNIX timestamp specified in milliseconds. 537 | */ 538 | pexpireatAsync(key: string, millisecondsTimestamp: number): Promise; 539 | 540 | /** 541 | * Adds the specified elements to the specified HyperLogLog. 542 | */ 543 | pfaddAsync: PromisedOverloadedKeyCommand; 544 | 545 | /** 546 | * Return the approximated cardinality of the set(s) observed by the HyperLogLog at key(s). 547 | */ 548 | pfcountAsync: PromisedOverloadedCommand; 549 | 550 | /** 551 | * Merge N different HyperLogLogs into a single one. 552 | */ 553 | pfmergeAsync: PromisedOverloadedCommand; 554 | 555 | /** 556 | * Set the value and expiration in milliseconds of a key. 557 | */ 558 | psetexAsync(key: string, milliseconds: number, value: string): Promise<"OK">; 559 | 560 | /** 561 | * Inspect the state of the Pub/Sub subsytem. 562 | */ 563 | pubsubAsync: PromisedOverloadedCommand; 564 | 565 | /** 566 | * Get the time to live for a key in milliseconds. 567 | */ 568 | pttlAsync(key: string): Promise; 569 | 570 | /** 571 | * Close the connection. 572 | */ 573 | quitAsync(): Promise<"OK">; 574 | 575 | /** 576 | * Return a random key from the keyspace. 577 | */ 578 | randomkeyAsync(): Promise; 579 | 580 | /** 581 | * Enables read queries for a connection to a cluster slave node. 582 | */ 583 | readonlyAsync(): Promise; 584 | 585 | /** 586 | * Disables read queries for a connection to cluster slave node. 587 | */ 588 | readwriteAsync(): Promise; 589 | 590 | /** 591 | * Rename a key. 592 | */ 593 | renameAsync(key: string, newkey: string): Promise<"OK">; 594 | 595 | /** 596 | * Rename a key, only if the new key does not exist. 597 | */ 598 | renamenxAsync(key: string, newkey: string): Promise; 599 | 600 | /** 601 | * Create a key using the provided serialized value, previously obtained using DUMP. 602 | */ 603 | restoreAsync(key: string, ttl: number, serializedValue: string): Promise<"OK">; 604 | 605 | /** 606 | * Return the role of the instance in the context of replication. 607 | */ 608 | roleAsync(): Promise<[string, number, Array<[string, string, string]>]>; 609 | 610 | /** 611 | * Remove and get the last element in a list. 612 | */ 613 | rpopAsync(key: string): Promise; 614 | 615 | /** 616 | * Remove the last element in a list, prepend it to another list and return it. 617 | */ 618 | rpoplpushAsync(source: string, destination: string): Promise; 619 | 620 | /** 621 | * Append one or multiple values to a list. 622 | */ 623 | rpushAsync: PromisedOverloadedKeyCommand; 624 | 625 | /** 626 | * Append a value to a list, only if the list exists. 627 | */ 628 | rpushxAsync(key: string, value: string): Promise; 629 | 630 | /** 631 | * Append one or multiple members to a set. 632 | */ 633 | saddAsync: PromisedOverloadedKeyCommand; 634 | 635 | /** 636 | * Synchronously save the dataset to disk. 637 | */ 638 | saveAsync(): Promise; 639 | 640 | /** 641 | * Get the number of members in a set. 642 | */ 643 | scardAsync(key: string): Promise; 644 | 645 | /** 646 | * DEBUG - Set the debug mode for executed scripts. 647 | * EXISTS - Check existence of scripts in the script cache. 648 | * FLUSH - Remove all scripts from the script cache. 649 | * KILL - Kill the script currently in execution. 650 | * LOAD - Load the specified Lua script into the script cache. 651 | */ 652 | scriptAsync: PromisedOverloadedCommand; 653 | 654 | /** 655 | * Subtract multiple sets. 656 | */ 657 | sdiffAsync: PromisedOverloadedCommand; 658 | 659 | /** 660 | * Subtract multiple sets and store the resulting set in a key. 661 | */ 662 | sdiffstoreAsync: PromisedOverloadedKeyCommand; 663 | 664 | /** 665 | * Change the selected database for the current connection. 666 | */ 667 | selectAsync(index: number | string): Promise; 668 | 669 | /** 670 | * Set the string value of a key. 671 | */ 672 | setAsync(key: string, value: string): Promise<"OK">; 673 | setAsync(key: string, value: string, flag: string): Promise<"OK">; 674 | setAsync(key: string, value: string, mode: string, duration: number): Promise<"OK" | undefined>; 675 | setAsync(key: string, value: string, mode: string, duration: number, flag: string): Promise<"OK" | undefined>; 676 | 677 | /** 678 | * Sets or clears the bit at offset in the string value stored at key. 679 | */ 680 | setbitAsync(key: string, offset: number, value: string): Promise; 681 | 682 | /** 683 | * Set the value and expiration of a key. 684 | */ 685 | setexAsync(key: string, seconds: number, value: string): Promise; 686 | 687 | /** 688 | * Set the value of a key, only if the key does not exist. 689 | */ 690 | setnxAsync(key: string, value: string): Promise; 691 | 692 | /** 693 | * Overwrite part of a string at key starting at the specified offset. 694 | */ 695 | setrangeAsync(key: string, offset: number, value: string): Promise; 696 | 697 | /** 698 | * Synchronously save the dataset to disk and then shut down the server. 699 | */ 700 | shutdownAsync: PromisedOverloadedCommand; 701 | 702 | /** 703 | * Intersect multiple sets. 704 | */ 705 | sinterAsync: PromisedOverloadedKeyCommand; 706 | 707 | /** 708 | * Intersect multiple sets and store the resulting set in a key. 709 | */ 710 | sinterstoreAsync: PromisedOverloadedCommand; 711 | 712 | /** 713 | * Determine if a given value is a member of a set. 714 | */ 715 | sismemberAsync(key: string, member: string): Promise; 716 | 717 | /** 718 | * Make the server a slave of another instance, or promote it as master. 719 | */ 720 | slaveofAsync(host: string, port: string | number): Promise; 721 | 722 | /** 723 | * Manages the Redis slow queries log. 724 | */ 725 | slowlogAsync: PromisedOverloadedCommand>; 726 | 727 | /** 728 | * Get all the members in a set. 729 | */ 730 | smembersAsync(key: string): Promise; 731 | 732 | /** 733 | * Move a member from one set to another. 734 | */ 735 | smoveAsync(source: string, destination: string, member: string): Promise; 736 | 737 | /** 738 | * Sort the elements in a list, set or sorted set. 739 | */ 740 | sortAsync: PromisedOverloadedCommand; 741 | 742 | /** 743 | * Remove and return one or multiple random members from a set. 744 | */ 745 | spopAsync(key: string): Promise; 746 | spopAsync(key: string, count: number): Promise; 747 | 748 | /** 749 | * Get one or multiple random members from a set. 750 | */ 751 | srandmemberAsync(key: string): Promise; 752 | srandmemberAsync(key: string, count: number): Promise; 753 | 754 | /** 755 | * Remove one or more members from a set. 756 | */ 757 | sremAsync: PromisedOverloadedKeyCommand; 758 | 759 | /** 760 | * Get the length of the value stored in a key. 761 | */ 762 | strlenAsync(key: string): Promise; 763 | 764 | /** 765 | * Add multiple sets. 766 | */ 767 | sunionAsync: PromisedOverloadedCommand; 768 | 769 | /** 770 | * Add multiple sets and store the resulting set in a key. 771 | */ 772 | sunionstoreAsync: PromisedOverloadedCommand; 773 | 774 | /** 775 | * Internal command used for replication. 776 | */ 777 | syncAsync(): Promise; 778 | 779 | /** 780 | * Return the current server time. 781 | */ 782 | timeAsync(): Promise<[string, string]>; 783 | 784 | /** 785 | * Get the time to live for a key. 786 | */ 787 | ttlAsync(key: string): Promise; 788 | 789 | /** 790 | * Determine the type stored at key. 791 | */ 792 | typeAsync(key: string): Promise; 793 | 794 | /** 795 | * Forget about all watched keys. 796 | */ 797 | unwatchAsync(): Promise<"OK">; 798 | 799 | /** 800 | * Wait for the synchronous replication of all the write commands sent in the context of the current connection. 801 | */ 802 | waitAsync(numslaves: number, timeout: number): Promise; 803 | 804 | /** 805 | * Watch the given keys to determine execution of the MULTI/EXEC block. 806 | */ 807 | watchAsync: PromisedOverloadedCommand; 808 | 809 | /** 810 | * Add one or more members to a sorted set, or update its score if it already exists. 811 | */ 812 | zaddAsync: PromisedOverloadedKeyCommand; 813 | 814 | /** 815 | * Get the number of members in a sorted set. 816 | */ 817 | zcardAsync(key: string): Promise; 818 | 819 | /** 820 | * Count the members in a sorted set with scores between the given values. 821 | */ 822 | zcountAsync(key: string, min: number | string, max: number | string): Promise; 823 | 824 | /** 825 | * Increment the score of a member in a sorted set. 826 | */ 827 | zincrbyAsync(key: string, increment: number, member: string): Promise; 828 | 829 | /** 830 | * Intersect multiple sorted sets and store the resulting sorted set in a new key. 831 | */ 832 | zinterstoreAsync: PromisedOverloadedCommand; 833 | 834 | /** 835 | * Count the number of members in a sorted set between a given lexicographic range. 836 | */ 837 | zlexcountAsync(key: string, min: string, max: string): Promise; 838 | 839 | /** 840 | * Return a range of members in a sorted set, by index. 841 | */ 842 | zrangeAsync(key: string, start: number, stop: number): Promise; 843 | zrangeAsync(key: string, start: number, stop: number, withscores: string): Promise; 844 | 845 | /** 846 | * Return a range of members in a sorted set, by lexicographical range. 847 | */ 848 | zrangebylexAsync(key: string, min: string, max: string): Promise; 849 | zrangebylexAsync(key: string, min: string, max: string, limit: string, offset: number, count: number): Promise; 850 | 851 | /** 852 | * Return a range of members in a sorted set, by lexicographical range, ordered from higher to lower strings. 853 | */ 854 | zrevrangebylexAsync(key: string, min: string, max: string): Promise; 855 | zrevrangebylexAsync(key: string, min: string, max: string, limit: string, offset: number, count: number): Promise; 856 | 857 | /** 858 | * Return a range of members in a sorted set, by score. 859 | */ 860 | zrangebyscoreAsync(key: string, min: number | string, max: number | string): Promise; 861 | zrangebyscoreAsync(key: string, min: number | string, max: number | string, withscores: string): Promise; 862 | zrangebyscoreAsync(key: string, min: number | string, max: number | string, limit: string, offset: number, count: number): Promise; 863 | zrangebyscoreAsync(key: string, min: number | string, max: number | string, withscores: string, limit: string, offset: number, count: number): Promise; 864 | 865 | /** 866 | * Determine the index of a member in a sorted set. 867 | */ 868 | zrankAsync(key: string, member: string): Promise; 869 | 870 | /** 871 | * Remove one or more members from a sorted set. 872 | */ 873 | zremAsync: PromisedOverloadedKeyCommand; 874 | 875 | /** 876 | * Remove all members in a sorted set between the given lexicographical range. 877 | */ 878 | zremrangebylexAsync(key: string, min: string, max: string): Promise; 879 | 880 | /** 881 | * Remove all members in a sorted set within the given indexes. 882 | */ 883 | zremrangebyrankAsync(key: string, start: number, stop: number): Promise; 884 | 885 | /** 886 | * Remove all members in a sorted set within the given indexes. 887 | */ 888 | zremrangebyscoreAsync(key: string, min: string | number, max: string | number): Promise; 889 | 890 | /** 891 | * Return a range of members in a sorted set, by index, with scores ordered from high to low. 892 | */ 893 | zrevrangeAsync(key: string, start: number, stop: number): Promise; 894 | zrevrangeAsync(key: string, start: number, stop: number, withscores: string): Promise; 895 | 896 | /** 897 | * Return a range of members in a sorted set, by score, with scores ordered from high to low. 898 | */ 899 | zrevrangebyscoreAsync(key: string, min: number | string, max: number | string): Promise; 900 | zrevrangebyscoreAsync(key: string, min: number | string, max: number | string, withscores: string): Promise; 901 | zrevrangebyscoreAsync(key: string, min: number | string, max: number | string, limit: string, offset: number, count: number): Promise; 902 | zrevrangebyscoreAsync(key: string, min: number | string, max: number | string, withscores: string, limit: string, offset: number, count: number): Promise; 903 | 904 | /** 905 | * Determine the index of a member in a sorted set, with scores ordered from high to low. 906 | */ 907 | zrevrankAsync(key: string, member: string): Promise; 908 | 909 | /** 910 | * Get the score associated with the given member in a sorted set. 911 | */ 912 | zscoreAsync(key: string, member: string): Promise; 913 | 914 | /** 915 | * Add multiple sorted sets and store the resulting sorted set in a new key. 916 | */ 917 | zunionstoreAsync: PromisedOverloadedCommand; 918 | 919 | /** 920 | * Incrementally iterate the keys space. 921 | */ 922 | scanAsync: PromisedOverloadedCommand; 923 | 924 | /** 925 | * Incrementally iterate Set elements. 926 | */ 927 | sscanAsync: PromisedOverloadedKeyCommand; 928 | 929 | /** 930 | * Incrementally iterate hash fields and associated values. 931 | */ 932 | hscanAsync: PromisedOverloadedKeyCommand; 933 | 934 | /** 935 | * Incrementally iterate sorted sets elements and associated scores. 936 | */ 937 | zscanAsync: PromisedOverloadedKeyCommand; 938 | } 939 | // tslint:enable:member-ordering 940 | 941 | declare module "redis" { 942 | interface RedisClient extends PromisedCommands { } 943 | } 944 | 945 | // Promisify redis stuff 946 | const RedisPromised = promisifyAll(RedisClient.prototype); 947 | Object.assign(RedisClient.prototype, RedisPromised); 948 | 949 | export * from "redis"; --------------------------------------------------------------------------------