Comparing version 1.2.0 to 2.0.0
@@ -22,2 +22,3 @@ export interface Client { | ||
zrange(key: string, start: number, stop: number): Promise<string[]>; | ||
zrangebyscore(key: string, min: string, max: string, offset: number, count: number): Promise<string[]>; | ||
zrevrange(key: string, start: number, stop: number): Promise<string[]>; | ||
@@ -29,2 +30,3 @@ lrange(key: string, start: number, stop: number): Promise<string[]>; | ||
hmget(key: string, properties: string[]): Promise<string[]>; | ||
eval(...args: any[]): Promise<any[]>; | ||
} |
@@ -25,2 +25,3 @@ import { Client } from './client'; | ||
sunion(keys: string[]): Promise<string[]>; | ||
zrangebyscore(key: string, min: string, max: string, offset: number, count: number): Promise<string[]>; | ||
zrange(key: string, start: number, stop: number): Promise<string[]>; | ||
@@ -33,2 +34,3 @@ zrevrange(key: string, start: number, stop: number): Promise<string[]>; | ||
hmget(key: string, properties: string[]): Promise<string[]>; | ||
eval(...args: any[]): Promise<any[]>; | ||
} |
@@ -103,2 +103,7 @@ "use strict"; | ||
} | ||
zrangebyscore(key, min, max, offset, count) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
return yield this.client.zrangebyscoreAsync(key, min, max, 'LIMIT', offset, count); | ||
}); | ||
} | ||
zrange(key, start, stop) { | ||
@@ -139,3 +144,8 @@ return __awaiter(this, void 0, void 0, function* () { | ||
} | ||
eval(...args) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
return yield this.client.evalAsync(args); | ||
}); | ||
} | ||
} | ||
exports.RedisClient = RedisClient; |
export * from './entity.decorator'; | ||
export * from './index.decorator'; | ||
export * from './primary.decorator'; | ||
@@ -4,0 +3,0 @@ export * from './property.decorator'; |
@@ -7,3 +7,2 @@ "use strict"; | ||
__export(require("./entity.decorator")); | ||
__export(require("./index.decorator")); | ||
__export(require("./primary.decorator")); | ||
@@ -10,0 +9,0 @@ __export(require("./property.decorator")); |
import 'reflect-metadata'; | ||
export declare function Property(options?: { | ||
sortable: boolean; | ||
searchable: boolean; | ||
searchable?: boolean; | ||
indexed?: boolean; | ||
}): Function; |
@@ -6,21 +6,18 @@ "use strict"; | ||
function Property(options = { | ||
sortable: false, | ||
searchable: false, | ||
indexed: false, | ||
}) { | ||
return (object, propertyName) => { | ||
const type = Reflect.getMetadata('design:type', object, propertyName).name; | ||
if (options.sortable && (type !== 'Date' && type !== 'Number')) { | ||
throw new Error('You can only make Dates and numbers sortables'); | ||
} | ||
if (metadata_storage_1.MetadataStorage.getGlobal().properties[object.constructor.name] === undefined) { | ||
metadata_storage_1.MetadataStorage.getGlobal().properties[object.constructor.name] = []; | ||
metadata_storage_1.MetadataStorage.getGlobal().properties[object.constructor.name] = {}; | ||
} | ||
metadata_storage_1.MetadataStorage.getGlobal().properties[object.constructor.name].push({ | ||
metadata_storage_1.MetadataStorage.getGlobal().properties[object.constructor.name][propertyName] = { | ||
name: propertyName, | ||
sortable: options.sortable, | ||
searchable: options.searchable, | ||
indexed: options.indexed, | ||
type, | ||
}); | ||
}; | ||
}; | ||
} | ||
exports.Property = Property; |
@@ -6,5 +6,6 @@ import { PropertyMetadata } from './property.metadata'; | ||
primary: string; | ||
indexes: string[]; | ||
uniques: string[]; | ||
properties: PropertyMetadata[]; | ||
properties: { | ||
[key: string]: PropertyMetadata; | ||
}; | ||
canBeListed: boolean; | ||
@@ -11,0 +12,0 @@ hasOneRelations: { |
@@ -15,3 +15,3 @@ "use strict"; | ||
getEntityMetadata(entityName) { | ||
const { names, indexes, primary, properties, canBeListed, uniques, hasOneRelations } = metadata_storage_1.MetadataStorage.getGlobal(); | ||
const { names, primary, properties, canBeListed, uniques, hasOneRelations } = metadata_storage_1.MetadataStorage.getGlobal(); | ||
if (names[entityName] === undefined) { | ||
@@ -26,3 +26,2 @@ throw new Error(entityName + ' is not an entity!'); | ||
primary: primary[entityName], | ||
indexes: indexes[entityName], | ||
uniques: uniques[entityName], | ||
@@ -29,0 +28,0 @@ properties: properties[entityName], |
@@ -12,3 +12,2 @@ "use strict"; | ||
canBeListed: {}, | ||
indexes: {}, | ||
uniques: {}, | ||
@@ -15,0 +14,0 @@ properties: {}, |
export interface PropertyMetadata { | ||
name: string; | ||
type: string; | ||
sortable: boolean; | ||
searchable: boolean; | ||
indexed: boolean; | ||
} |
@@ -10,5 +10,2 @@ import { PropertyMetadata } from './property.metadata'; | ||
}; | ||
indexes: { | ||
[key: string]: string[]; | ||
}; | ||
uniques: { | ||
@@ -18,3 +15,5 @@ [key: string]: string[]; | ||
properties: { | ||
[key: string]: PropertyMetadata[]; | ||
[key: string]: { | ||
[key: string]: PropertyMetadata; | ||
}; | ||
}; | ||
@@ -21,0 +20,0 @@ primary: { |
@@ -6,2 +6,3 @@ import { Type } from './metadata/type'; | ||
import { ClientOptions, Client } from './client'; | ||
import { WhereCondition } from './interfaces/where-condition'; | ||
export declare class Redisk { | ||
@@ -16,8 +17,12 @@ private readonly metadata; | ||
count<T>(entityType: Type<T>): Promise<number>; | ||
list<T>(entityType: Type<T>, limit?: number, offset?: number, orderBy?: OrderBy): Promise<T[]>; | ||
find<T>(entityType: Type<T>, conditions: Condition[], limit?: number, offset?: number, type?: 'AND' | 'OR'): Promise<T[]>; | ||
list<T>(entityType: Type<T>, where?: { | ||
conditions: WhereCondition[]; | ||
type: 'AND' | 'OR'; | ||
}, limit?: number, offset?: number, orderBy?: OrderBy): Promise<T[]>; | ||
search<T>(entityType: Type<T>, condition: Condition, limit: number): Promise<T[]>; | ||
searchIds<T>(entityType: Type<T>, condition: Condition, limit: number): Promise<string[]>; | ||
findIds<T>(entityType: Type<T>, conditions: Condition[], type?: 'AND' | 'OR'): Promise<string[]>; | ||
listIds<T>(entityType: Type<T>, limit?: number, offset?: number, orderBy?: OrderBy): Promise<string[]>; | ||
listIds<T>(entityType: Type<T>, where?: { | ||
conditions: WhereCondition[]; | ||
type: 'AND' | 'OR'; | ||
}, limit?: number, offset?: number, orderBy?: OrderBy): Promise<string[]>; | ||
delete<T>(entityType: Type<T>, id: string): Promise<void>; | ||
@@ -29,10 +34,11 @@ getOne<T>(entityType: Type<T>, value: any, key?: string): Promise<T>; | ||
private dropIndexes; | ||
private dropIndex; | ||
private dropSearchables; | ||
private dropSortables; | ||
private getIndexNumberKeyName; | ||
private getIndexKeyName; | ||
private getIndexPrefix; | ||
private getListKeyName; | ||
private getUniqueKeyName; | ||
private getSortableKeyName; | ||
private getSearchableKeyName; | ||
private getSearchableValuePrefix; | ||
} |
@@ -14,2 +14,4 @@ "use strict"; | ||
const client_1 = require("./client"); | ||
const fs = require("fs"); | ||
const path = require("path"); | ||
class Redisk { | ||
@@ -33,3 +35,3 @@ constructor(metadata, client) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const { name, uniques, primary, canBeListed, indexes, properties, hasOneRelations } = this.metadata.getEntityMetadataFromInstance(entity); | ||
const { name, uniques, primary, canBeListed, properties, hasOneRelations } = this.metadata.getEntityMetadataFromInstance(entity); | ||
const hashKey = name + ':' + entity[primary]; | ||
@@ -39,3 +41,3 @@ const persistedEntity = yield this.getOne(entity.constructor, entity[primary]); | ||
const changedFields = []; | ||
for (const property of properties) { | ||
for (const property of Object.keys(properties).map(key => properties[key])) { | ||
if (entity[property.name] !== persistedEntity[property.name]) { | ||
@@ -52,13 +54,7 @@ changedFields.push(property.name); | ||
} | ||
if (property.sortable) { | ||
yield this.client.zrem(this.getSortableKeyName(name, property.name), persistedEntity[property.name]); | ||
if (property.indexed) { | ||
yield this.dropIndex(persistedEntity, property, persistedEntity[primary]); | ||
} | ||
} | ||
} | ||
if (indexes) { | ||
const indexesChanged = changedFields.some(value => indexes.indexOf(value) >= 0); | ||
if (indexesChanged) { | ||
yield this.dropIndexes(persistedEntity, entity[primary]); | ||
} | ||
} | ||
if (uniques) { | ||
@@ -83,3 +79,3 @@ const uniquesChanged = changedFields.some(value => uniques.indexOf(value) >= 0); | ||
const valuesToStore = []; | ||
for (const property of properties) { | ||
for (const property of Object.keys(properties).map(key => properties[key])) { | ||
if (entity[property.name] !== null) { | ||
@@ -96,23 +92,23 @@ let valueToStore = this.convertPropertyTypeToPrimitive(property, entity[property.name]); | ||
valuesToStore.push(valueToStore); | ||
if (property.sortable === true) { | ||
yield this.client.zadd(this.getSortableKeyName(name, property.name), this.convertPropertyTypeToPrimitive(property, entity[property.name]), entity[primary]); | ||
} | ||
if (property.searchable === true) { | ||
if (property.searchable) { | ||
yield this.client.sadd(this.getSearchableKeyName(name, property.name), this.getSearchableValuePrefix(entity[primary]) + entity[property.name].toLowerCase()); | ||
} | ||
if (property.indexed) { | ||
let value = entity[property.name]; | ||
if (hasOneRelations !== undefined && hasOneRelations[property.name] && entity[property.name] !== null) { | ||
const relatedEntity = this.metadata.getEntityMetadataFromName(hasOneRelations[property.name].entity); | ||
value = entity[property.name][relatedEntity.primary]; | ||
} | ||
if (value !== null) { | ||
if (property.type === 'Date' || property.type === 'Number') { | ||
yield this.client.zadd(this.getIndexNumberKeyName(name, property.name), this.convertPropertyTypeToPrimitive(property, entity[property.name]), entity[primary]); | ||
} | ||
else { | ||
yield this.client.zadd(this.getIndexKeyName(name, property.name, value), '0', entity[primary]); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
yield this.client.hmset(hashKey, valuesToStore); | ||
if (indexes) { | ||
for (const indexName of indexes) { | ||
let value = entity[indexName]; | ||
if (hasOneRelations !== undefined && hasOneRelations[indexName] && entity[indexName] !== null) { | ||
const relatedEntity = this.metadata.getEntityMetadataFromName(hasOneRelations[indexName].entity); | ||
value = entity[indexName][relatedEntity.primary]; | ||
} | ||
if (value !== null) { | ||
yield this.client.sadd(this.getIndexKeyName(name, indexName, value), entity[primary]); | ||
} | ||
} | ||
} | ||
if (canBeListed && persistedEntity === null) { | ||
@@ -131,5 +127,5 @@ yield this.client.rpush(this.getListKeyName(name), entity[primary]); | ||
} | ||
list(entityType, limit, offset, orderBy) { | ||
list(entityType, where, limit, offset, orderBy) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const ids = yield this.listIds(entityType, limit, offset, orderBy); | ||
const ids = yield this.listIds(entityType, where, limit, offset, orderBy); | ||
const response = []; | ||
@@ -142,22 +138,2 @@ for (const id of ids) { | ||
} | ||
find(entityType, conditions, limit, offset, type = 'AND') { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const ids = yield this.findIds(entityType, conditions, type); | ||
const response = []; | ||
if (limit !== undefined || offset !== undefined) { | ||
if (limit === undefined || offset === undefined) { | ||
throw new Error('You must specify limit and offset, not just one arg.'); | ||
} | ||
for (let index = offset; index < ids.length && index < (limit + offset); index++) { | ||
response.push(yield this.getOne(entityType, ids[index])); | ||
} | ||
} | ||
else { | ||
for (const id of ids) { | ||
response.push(yield this.getOne(entityType, id)); | ||
} | ||
} | ||
return response; | ||
}); | ||
} | ||
search(entityType, condition, limit) { | ||
@@ -193,23 +169,5 @@ return __awaiter(this, void 0, void 0, function* () { | ||
} | ||
findIds(entityType, conditions, type = 'AND') { | ||
listIds(entityType, where, limit, offset, orderBy) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
if (conditions.length === 0) { | ||
throw new Error('You should at least specify one key to search'); | ||
} | ||
const { name } = this.metadata.getEntityMetadataFromType(entityType); | ||
const keyNames = []; | ||
for (const condition of conditions) { | ||
keyNames.push(this.getIndexKeyName(name, condition.key, String(condition.value))); | ||
} | ||
if (type === 'AND') { | ||
return yield this.client.sinter(keyNames); | ||
} | ||
else { | ||
return yield this.client.sunion(keyNames); | ||
} | ||
}); | ||
} | ||
listIds(entityType, limit, offset, orderBy) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const { name, canBeListed } = this.metadata.getEntityMetadataFromType(entityType); | ||
const { name, canBeListed, properties } = this.metadata.getEntityMetadataFromType(entityType); | ||
if (!canBeListed) { | ||
@@ -227,4 +185,4 @@ throw new Error(entityType.name + ' can\'t be listed!'); | ||
} | ||
if (orderBy !== undefined) { | ||
const sortableKey = this.getSortableKeyName(name, orderBy.field); | ||
if (orderBy !== undefined && where === undefined) { | ||
const sortableKey = this.getIndexNumberKeyName(name, orderBy.field); | ||
if (orderBy.strategy === 'ASC') { | ||
@@ -237,2 +195,74 @@ return yield this.client.zrange(sortableKey, start, stop); | ||
} | ||
if (where !== undefined) { | ||
if (where.conditions.length === 0) { | ||
throw new Error('Conditions can\'t be empty'); | ||
} | ||
const scores = {}; | ||
const equals = []; | ||
for (const condition of where.conditions) { | ||
if (condition.comparator === '>') { | ||
if (!scores[condition.key]) { | ||
scores[condition.key] = { min: '-inf', max: '+inf' }; | ||
} | ||
scores[condition.key].min = this.convertPropertyTypeToPrimitive(properties[condition.key], condition.value); | ||
} | ||
if (condition.comparator === '<') { | ||
if (!scores[condition.key]) { | ||
scores[condition.key] = { min: '-inf', max: '+inf' }; | ||
} | ||
scores[condition.key].max = this.convertPropertyTypeToPrimitive(properties[condition.key], condition.value); | ||
} | ||
if (condition.comparator === '!=' || condition.comparator === '=') { | ||
if (properties[condition.key] === undefined || !properties[condition.key].indexed) { | ||
throw new Error('Property ' + condition.key + ' not found or not indexed'); | ||
} | ||
equals.push(condition); | ||
} | ||
} | ||
if (Object.keys(scores).length === 1 && equals.length === 0 && orderBy === undefined) { | ||
const scoreKey = Object.keys(scores)[0]; | ||
return yield this.client.zrangebyscore(this.getIndexNumberKeyName(name, scoreKey), scores[scoreKey].min, scores[scoreKey].max, offset ? offset : 0, limit ? limit : -1); | ||
} | ||
if (equals.length === 1 && Object.keys(scores).length === 0 && orderBy === undefined && equals[0].comparator != "!=") { | ||
const condition = equals[0]; | ||
return yield this.client.zrange(this.getIndexKeyName(name, condition.key, this.convertPropertyTypeToPrimitive(properties[condition.key], condition.value)), start, stop); | ||
} | ||
let luaOrderBy = undefined; | ||
if (orderBy !== undefined) { | ||
for (const scoreKey in scores) { | ||
if (scoreKey === orderBy.field) { | ||
luaOrderBy = { | ||
name: scoreKey, | ||
min: String(scores[scoreKey].min), | ||
max: String(scores[scoreKey].max), | ||
strategy: orderBy.strategy, | ||
}; | ||
} | ||
} | ||
if (luaOrderBy === undefined) { | ||
luaOrderBy = { | ||
name: orderBy.field, | ||
strategy: orderBy.strategy, | ||
min: "-inf", | ||
max: "+inf", | ||
}; | ||
} | ||
} | ||
let luaArgs = { | ||
prefix: this.getIndexPrefix(name), | ||
listKey: this.getListKeyName(name), | ||
tempPrefix: 'temp:' + name + ':', | ||
orderBy: luaOrderBy, | ||
scores: Object.keys(scores).map((key) => ({ | ||
min: scores[key].min, | ||
max: scores[key].max, | ||
key, | ||
})), | ||
equals, | ||
limit: limit ? limit : -1, | ||
offset: offset ? offset : 0, | ||
type: where.type, | ||
}; | ||
return yield this.client.eval(fs.readFileSync(path.join(__dirname, './lua/complex.query.lua')), 0, JSON.stringify(luaArgs)); | ||
} | ||
return yield this.client.lrange(keyName, start, stop); | ||
@@ -243,3 +273,3 @@ }); | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const { name, uniques, indexes, canBeListed } = this.metadata.getEntityMetadataFromType(entityType); | ||
const { name, uniques, canBeListed } = this.metadata.getEntityMetadataFromType(entityType); | ||
const hashKey = name + ':' + id; | ||
@@ -250,9 +280,6 @@ const persistedEntity = yield this.getOne(entityType, id); | ||
} | ||
if (indexes) { | ||
yield this.dropIndexes(persistedEntity, id); | ||
} | ||
if (canBeListed) { | ||
yield this.client.lrem(this.getListKeyName(name), 1, id); | ||
} | ||
yield this.dropSortables(persistedEntity); | ||
yield this.dropIndexes(persistedEntity, id); | ||
yield this.dropSearchables(persistedEntity); | ||
@@ -284,10 +311,11 @@ yield this.client.del(hashKey); | ||
const hashKey = name + ':' + id; | ||
const result = yield this.client.hmget(hashKey, properties.map((property) => property.name)); | ||
const result = yield this.client.hmget(hashKey, Object.keys(properties).map(key => properties[key].name)); | ||
const propertiesArray = Object.keys(properties).map(key => properties[key]); | ||
let index = 0; | ||
for (const resultKey of result) { | ||
if (hasOneRelations !== undefined && hasOneRelations[properties[index].name] && resultKey !== null) { | ||
entity[properties[index].name] = yield this.getOne(hasOneRelations[properties[index].name].entityType, resultKey); | ||
if (hasOneRelations !== undefined && hasOneRelations[propertiesArray[index].name] && resultKey !== null) { | ||
entity[propertiesArray[index].name] = yield this.getOne(hasOneRelations[propertiesArray[index].name].entityType, resultKey); | ||
} | ||
else { | ||
entity[properties[index].name] = this.convertStringToPropertyType(properties[index], resultKey); | ||
entity[propertiesArray[index].name] = this.convertStringToPropertyType(propertiesArray[index], resultKey); | ||
} | ||
@@ -333,31 +361,30 @@ index++; | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const { name, indexes, hasOneRelations } = this.metadata.getEntityMetadataFromInstance(entity); | ||
if (indexes) { | ||
for (const indexName of indexes) { | ||
let value = entity[indexName]; | ||
if (hasOneRelations !== undefined && hasOneRelations[indexName]) { | ||
const relatedEntity = this.metadata.getEntityMetadataFromName(hasOneRelations[indexName].entity); | ||
value = entity[indexName][relatedEntity.primary]; | ||
} | ||
yield this.client.srem(this.getIndexKeyName(name, indexName, value), id); | ||
} | ||
const { properties } = this.metadata.getEntityMetadataFromInstance(entity); | ||
for (const property of Object.keys(properties).filter(key => properties[key].indexed).map(key => properties[key])) { | ||
yield this.dropIndex(entity, property, id); | ||
} | ||
}); | ||
} | ||
dropSearchables(entity) { | ||
dropIndex(entity, property, id) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const { name, properties, primary } = this.metadata.getEntityMetadataFromInstance(entity); | ||
for (const property of properties) { | ||
if (property.searchable === true) { | ||
yield this.client.srem(this.getSearchableKeyName(name, property.name), this.getSearchableValuePrefix(entity[primary]) + entity[property.name].toLowerCase()); | ||
} | ||
const { name, hasOneRelations } = this.metadata.getEntityMetadataFromInstance(entity); | ||
let value = entity[property.name]; | ||
if (hasOneRelations !== undefined && hasOneRelations[property.name]) { | ||
const relatedEntity = this.metadata.getEntityMetadataFromName(hasOneRelations[property.name].entity); | ||
value = entity[property.name][relatedEntity.primary]; | ||
} | ||
if (property.type === 'Date' || property.type === 'Number') { | ||
yield this.client.zrem(this.getIndexNumberKeyName(name, property.name), id); | ||
} | ||
else { | ||
yield this.client.zrem(this.getIndexKeyName(name, property.name, value), id); | ||
} | ||
}); | ||
} | ||
dropSortables(entity) { | ||
dropSearchables(entity) { | ||
return __awaiter(this, void 0, void 0, function* () { | ||
const { name, properties, primary } = this.metadata.getEntityMetadataFromInstance(entity); | ||
for (const property of properties) { | ||
if (property.sortable === true) { | ||
yield this.client.zrem(this.getSortableKeyName(name, property.name), entity[primary]); | ||
for (const property of Object.keys(properties).map(key => properties[key])) { | ||
if (property.searchable) { | ||
yield this.client.srem(this.getSearchableKeyName(name, property.name), this.getSearchableValuePrefix(entity[primary]) + entity[property.name].toLowerCase()); | ||
} | ||
@@ -367,5 +394,11 @@ } | ||
} | ||
getIndexNumberKeyName(entityName, indexName) { | ||
return this.getIndexPrefix(entityName) + indexName; | ||
} | ||
getIndexKeyName(entityName, indexName, indexValue) { | ||
return entityName + ':index:' + indexName + ':' + indexValue; | ||
return this.getIndexPrefix(entityName) + indexName + ':' + indexValue; | ||
} | ||
getIndexPrefix(entityName) { | ||
return entityName + ':index:'; | ||
} | ||
getListKeyName(entityName) { | ||
@@ -377,5 +410,2 @@ return entityName + ':list'; | ||
} | ||
getSortableKeyName(entityName, fieldName) { | ||
return entityName + ':sort:' + fieldName; | ||
} | ||
getSearchableKeyName(entityName, fieldName) { | ||
@@ -382,0 +412,0 @@ return entityName + ':search:' + fieldName; |
{ | ||
"name": "redisk", | ||
"version": "1.2.0", | ||
"version": "2.0.0", | ||
"description": "TypeScript ORM for Redis.", | ||
@@ -11,3 +11,3 @@ "main": "index.js", | ||
"prebuild": "rm -rf dist", | ||
"build": "tsc -p tsconfig.json", | ||
"build": "tsc -p tsconfig.json && cpx 'src/lua/*.lua' dist/lua", | ||
"prepublish:npm": "npm run build", | ||
@@ -31,2 +31,4 @@ "publish:npm": "npm publish --access public", | ||
"redis", | ||
"redisk", | ||
"ts", | ||
"orm" | ||
@@ -42,2 +44,4 @@ ], | ||
"@types/jest": "^24.9.1", | ||
"@types/node": "^13.9.0", | ||
"cpx": "^1.5.0", | ||
"jest": "^25.1.0", | ||
@@ -44,0 +48,0 @@ "ts-jest": "^25.0.0", |
214
README.md
@@ -1,3 +0,5 @@ | ||
Redisk | ||
===== | ||
<h1 align="center"> | ||
<img src="https://raw.githubusercontent.com/arkerlabs/redisk/master/docs/images/logo.png" alt="Redisk"> | ||
</h1> | ||
[![npm version](https://badge.fury.io/js/redisk.svg)](https://badge.fury.io/js/redisk) | ||
@@ -16,3 +18,3 @@ | ||
* List entities with common filters, like limit, count and sort by. | ||
* Find entities with multiple conditions. | ||
* Find entities with multiple conditions ('>', '<', '=', '!='). | ||
* Search (Similar to LIKE in SQL) | ||
@@ -34,3 +36,3 @@ * And much more. | ||
@Property() | ||
public readonly name: string; | ||
public name: string; | ||
@@ -61,23 +63,23 @@ constructor( | ||
- [Connection](#connection) | ||
- - [Options](#options) | ||
- [Options](#options) | ||
- [Models](#models) | ||
- - [Model definition](#model-definition) | ||
- - [Entity](#entity) | ||
- - [Property](#property) | ||
- - - [Supported types](#supported-types) | ||
- - [Primary](#primary) | ||
- - [Unique](#unique) | ||
- - [Index](#index) | ||
- - [Embedding other entities](#embedding-other-entities) | ||
- [Model definition](#model-definition) | ||
- [Entity](#entity) | ||
- [Property](#property) | ||
- [Supported types](#supported-types) | ||
- [Primary](#primary) | ||
- [Unique](#unique) | ||
- [Embedding other entities](#embedding-other-entities) | ||
- [Queries](#queries) | ||
- - [Save and Update](#save-and-update) | ||
- - [Get by primary key](#get-by-primary-key) | ||
- - [Get by unique key](#get-by-unique-key) | ||
- - [Count](#count) | ||
- - [List all](#list-all) | ||
- - [Find all by index](#find-all-by-index) | ||
- - - [Simple](#simple) | ||
- - - [Multiple conditions](#multiple-conditions) | ||
- - [Pattern matching](#pattern-matching) | ||
- - [Delete](#delete) | ||
- [Save](#save) | ||
- [Update](#update) | ||
- [Get by primary key](#get-by-primary-key) | ||
- [Get by unique key](#get-by-unique-key) | ||
- [Count](#count) | ||
- [List all](#list-all) | ||
- [List all with conditions](#find-all-by-index) | ||
- [Simple](#simple) | ||
- [Multiple conditions](#multiple-conditions) | ||
- [Pattern matching](#pattern-matching) | ||
- [Delete](#delete) | ||
@@ -91,3 +93,3 @@ | ||
### `options` | ||
### Options | ||
| Property | Description | | ||
@@ -112,3 +114,3 @@ |----------|-----------------------------------------------------------------------------------------------------------------------------------------| | ||
```ts | ||
@Entity('user', { canBeListed: true }) | ||
@Entity('user') | ||
export class User { | ||
@@ -120,19 +122,18 @@ | ||
@Property({sortable: false, searchable: true}) | ||
public readonly name: string; | ||
@Property({searchable: true}) | ||
public name: string; | ||
@Unique() | ||
@Property() | ||
public readonly email: string; | ||
public email: string; | ||
@Index() | ||
@Property() | ||
public readonly color: string; | ||
@Property({indexed: true}) | ||
public color: string; | ||
@HasOne(Group, {cascadeInsert: true, cascadeUpdate: true}) | ||
@Property() | ||
public readonly group: Group; | ||
public group: Group; | ||
@Property({sortable: true, searchable: false}) | ||
public readonly created: Date; | ||
@Property({indexed: true}) | ||
public created: Date; | ||
@@ -160,3 +161,4 @@ constructor( | ||
You can pass the option canBeListed to 'false' (Default is true) to save some space. | ||
You can pass the option canBeListed to 'false' (Default is true) to save some space, but you will not be able to list user entities. | ||
```ts | ||
@@ -170,4 +172,5 @@ @Entity('user', { canBeListed: true }) | ||
The decorator `Property` is used to save the fields into redis. | ||
Optionally, you can pass the options `sortable` if you want to use the field to sort in the 'list' method or `searchable` if you want to use pattern matching in this field. | ||
Optionally, you can pass the options `indexed` if you want to use the field to sort or to use as a condition in the 'list' method or `searchable` if you want to use pattern matching in this field. | ||
Both options are false by default. | ||
@@ -179,3 +182,3 @@ | ||
@Property({sortable: true, searchable: false}) | ||
@Property({indexed: true, searchable: false}) | ||
public readonly created: Date; | ||
@@ -220,18 +223,6 @@ | ||
### Index | ||
Use the decorator `Index` on the fields that you want to query later with the find() method. | ||
```ts | ||
@Entity('user') | ||
export class User { | ||
@Index() | ||
@Property() | ||
public readonly color: string; | ||
} | ||
``` | ||
### Embedding other entities | ||
You can make one to one relations with the `HasOne` decorator. | ||
Cascade inserts and updates are supported. | ||
Cascade inserts and updates are supported. (These options are false by default) | ||
@@ -250,3 +241,3 @@ ```ts | ||
### Save and update | ||
### Save | ||
@@ -257,2 +248,10 @@ ```ts | ||
### Update | ||
```ts | ||
const user = await redisk.getOne(User, id); | ||
user.name = 'Bar'; | ||
await redisk.save(user); | ||
``` | ||
### Get by primary key | ||
@@ -279,54 +278,97 @@ | ||
### List all | ||
Returns an array of all user entities. | ||
```ts | ||
await redisk.list(User); | ||
``` | ||
Returns the first 10 user entities | ||
```ts | ||
await redisk.list(User); // Returns an array of entities | ||
const limit = 10; | ||
const offset = 0; | ||
await redis.list(User, limit, offset); // Returns 10 user entities | ||
await redis.list(User, limit, offset); | ||
``` | ||
await redisk.list(User, undefined, undefined, { | ||
Return an array of user entities sorted by his creation date in descending order | ||
```ts | ||
await redisk.list(User, undefined, undefined, undefined, { | ||
field: 'created', | ||
strategy: 'DESC', | ||
}); // Returns an array of entities sorted by his creation date in descending order | ||
}); | ||
``` | ||
### Find all by index | ||
### List all with conditions | ||
#### Simple | ||
Returns an array of users where his color is red | ||
```ts | ||
const conditions = [ | ||
{ | ||
key: 'color', | ||
value: 'red', | ||
}, | ||
]; | ||
await redisk.find(User, conditions, limit, offset); // Returns an array of entities that match the conditions | ||
const where = | ||
conditions: [ | ||
{ | ||
key: 'color', | ||
value: 'red', | ||
comparator: '=', | ||
}, | ||
], | ||
type: 'AND', | ||
}; | ||
await redisk.find(User, where, limit, offset); | ||
``` | ||
Returns an array of users where his creation date is greater than the day 23 | ||
```ts | ||
const where = | ||
conditions: [ | ||
{ | ||
key: 'created', | ||
value: new Date('2020-02-23 00:00:00'), | ||
comparator: '>', | ||
}, | ||
], | ||
type: 'AND', | ||
}; | ||
await redisk.find(User, where, limit, offset); | ||
``` | ||
#### Multiple conditions | ||
Returns an array of entities that his color field is 'red' or 'blue'. | ||
Warning: Using multiple conditions leads to multiple queries with table intersections, to achieve high performance queries try to reduce the results with more concise conditional. | ||
```ts | ||
const conditions = [ | ||
{ | ||
key: 'color', | ||
value: 'red', | ||
}, | ||
{ | ||
key: 'color', | ||
value: 'blue', | ||
}, | ||
]; | ||
await redisk.find(User, conditions, limit, offset, 'OR'); // Returns an array of entities that his color field is 'red' or 'blue' | ||
const where = | ||
conditions: [ | ||
{ | ||
key: 'color', | ||
value: 'red', | ||
comparator: '=', | ||
}, | ||
{ | ||
key: 'color', | ||
value: 'blue', | ||
comparator: '=', | ||
}, | ||
], | ||
type: 'OR', | ||
}; | ||
await redisk.find(User, where, limit, offset); | ||
``` | ||
Returns an array of entities that his color field is 'red' and his food field is 'avocado' | ||
```ts | ||
const conditions = [ | ||
{ | ||
key: 'color', | ||
value: 'red', | ||
}, | ||
{ | ||
key: 'food', | ||
value: 'avocado', | ||
}, | ||
]; | ||
await redisk.find(User, conditions, limit, offset, 'AND'); // Returns an array of entities that his color field is 'red' and his food field is 'avocado' | ||
const where = | ||
conditions: [ | ||
{ | ||
key: 'color', | ||
value: 'red', | ||
comparator: '=', | ||
}, | ||
{ | ||
key: 'color', | ||
value: 'blue', | ||
comparator: '=', | ||
}, | ||
], | ||
type: 'AND', | ||
}; | ||
await redisk.find(User, where, limit, offset); | ||
``` | ||
@@ -333,0 +375,0 @@ |
@@ -19,2 +19,3 @@ export interface Client { | ||
zrange(key: string, start: number, stop: number): Promise<string[]>; | ||
zrangebyscore(key: string, min: string, max: string, offset: number, count: number): Promise<string[]>; | ||
zrevrange(key: string, start: number, stop: number): Promise<string[]>; | ||
@@ -26,2 +27,3 @@ lrange(key: string, start: number, stop: number): Promise<string[]>; | ||
hmget(key: string, properties: string[]): Promise<string[]>; | ||
eval(...args: any[]): Promise<any[]>; | ||
} |
@@ -84,2 +84,6 @@ import { Client } from './client'; | ||
async zrangebyscore(key: string, min: string, max: string, offset: number, count: number): Promise<string[]> { | ||
return await this.client.zrangebyscoreAsync(key, min, max, 'LIMIT', offset, count); | ||
} | ||
async zrange(key: string, start: number, stop: number): Promise<string[]> { | ||
@@ -112,2 +116,6 @@ return await this.client.zrangeAsync(key, start, stop); | ||
} | ||
async eval(...args: any[]): Promise<any[]> { | ||
return await this.client.evalAsync(args); | ||
} | ||
} |
import { MetadataStorage } from '../metadata/metadata.storage'; | ||
import 'reflect-metadata'; | ||
export function Property(options: {sortable: boolean, searchable: boolean} = { | ||
sortable: false, | ||
export function Property(options: {searchable?: boolean, indexed?: boolean} = { | ||
searchable: false, | ||
indexed: false, | ||
// tslint:disable-next-line: ban-types | ||
@@ -12,16 +12,12 @@ }): Function { | ||
if (options.sortable && (type !== 'Date' && type !== 'Number')) { | ||
throw new Error('You can only make Dates and numbers sortables'); | ||
} | ||
if (MetadataStorage.getGlobal().properties[object.constructor.name] === undefined) { | ||
MetadataStorage.getGlobal().properties[object.constructor.name] = []; | ||
MetadataStorage.getGlobal().properties[object.constructor.name] = {}; | ||
} | ||
MetadataStorage.getGlobal().properties[object.constructor.name].push({ | ||
MetadataStorage.getGlobal().properties[object.constructor.name][propertyName] = { | ||
name: propertyName, | ||
sortable: options.sortable, | ||
searchable: options.searchable, | ||
indexed: options.indexed, | ||
type, | ||
}); | ||
}; | ||
}; | ||
} |
@@ -7,7 +7,6 @@ import { PropertyMetadata } from './property.metadata'; | ||
primary: string; | ||
indexes: string[]; | ||
uniques: string[]; | ||
properties: PropertyMetadata[]; | ||
properties: {[key: string] : PropertyMetadata}; | ||
canBeListed: boolean; | ||
hasOneRelations: {[key: string]: HasOneOptions }; | ||
} |
@@ -8,3 +8,2 @@ import { StateMetadata } from './state.metadata'; | ||
canBeListed: {}, | ||
indexes: {}, | ||
uniques: {}, | ||
@@ -11,0 +10,0 @@ properties: {}, |
@@ -20,3 +20,3 @@ import { MetadataStorage } from './metadata.storage'; | ||
private getEntityMetadata<T>(entityName: string): EntityMetadata { | ||
const { names, indexes, primary, properties, canBeListed, uniques, hasOneRelations } = MetadataStorage.getGlobal(); | ||
const { names, primary, properties, canBeListed, uniques, hasOneRelations } = MetadataStorage.getGlobal(); | ||
@@ -34,3 +34,2 @@ if (names[entityName] === undefined) { | ||
primary: primary[entityName], | ||
indexes: indexes[entityName], | ||
uniques: uniques[entityName], | ||
@@ -37,0 +36,0 @@ properties: properties[entityName], |
export interface PropertyMetadata { | ||
name: string; | ||
type: string; | ||
sortable: boolean; | ||
searchable: boolean; | ||
indexed: boolean; | ||
} |
@@ -7,7 +7,6 @@ import { PropertyMetadata } from './property.metadata'; | ||
canBeListed: { [key: string]: boolean; }; | ||
indexes: { [key: string]: string[]; }; | ||
uniques: { [key: string]: string[]; }; | ||
properties: { [key: string]: PropertyMetadata[]; }; | ||
properties: { [key: string]: { [key: string]: PropertyMetadata} }; | ||
primary: { [key: string]: string; }; | ||
hasOneRelations: { [key: string]: {[key: string]: HasOneOptions } } | ||
} |
@@ -7,2 +7,5 @@ import { Type } from './metadata/type'; | ||
import { ClientOptions, Client, RedisClient } from './client'; | ||
import { WhereCondition } from './interfaces/where-condition'; | ||
import * as fs from 'fs'; | ||
import * as path from 'path'; | ||
@@ -31,3 +34,3 @@ export class Redisk { | ||
const {name, uniques, primary, canBeListed, indexes, properties, hasOneRelations} = this.metadata.getEntityMetadataFromInstance(entity); | ||
const {name, uniques, primary, canBeListed, properties, hasOneRelations} = this.metadata.getEntityMetadataFromInstance(entity); | ||
@@ -40,3 +43,3 @@ const hashKey = name + ':' + entity[primary]; | ||
for (const property of properties) { | ||
for (const property of Object.keys(properties).map(key => properties[key])) { | ||
if (entity[property.name] !== persistedEntity[property.name]) { | ||
@@ -59,4 +62,5 @@ changedFields.push(property.name); | ||
} | ||
if (property.sortable) { | ||
await this.client.zrem(this.getSortableKeyName(name, property.name), persistedEntity[property.name]); | ||
if (property.indexed) { | ||
await this.dropIndex(persistedEntity, property, persistedEntity[primary]); | ||
} | ||
@@ -66,9 +70,2 @@ } | ||
if (indexes) { | ||
const indexesChanged = changedFields.some(value => indexes.indexOf(value) >= 0); | ||
if (indexesChanged) { | ||
await this.dropIndexes(persistedEntity, entity[primary]); | ||
} | ||
} | ||
if (uniques) { | ||
@@ -99,3 +96,3 @@ const uniquesChanged = changedFields.some(value => uniques.indexOf(value) >= 0); | ||
const valuesToStore = []; | ||
for (const property of properties) { | ||
for (const property of Object.keys(properties).map(key => properties[key])) { | ||
if (entity[property.name] !== null) { | ||
@@ -116,12 +113,4 @@ | ||
valuesToStore.push(valueToStore); | ||
if (property.sortable === true) { | ||
await this.client.zadd( | ||
this.getSortableKeyName(name, property.name), | ||
this.convertPropertyTypeToPrimitive(property, entity[property.name]), | ||
entity[primary], | ||
); | ||
} | ||
if (property.searchable === true) { | ||
if (property.searchable) { | ||
await this.client.sadd( | ||
@@ -132,18 +121,29 @@ this.getSearchableKeyName(name, property.name), | ||
} | ||
} | ||
} | ||
await this.client.hmset(hashKey, valuesToStore); | ||
if (indexes) { | ||
for (const indexName of indexes) { | ||
let value = entity[indexName]; | ||
if (hasOneRelations !== undefined && hasOneRelations[indexName] && entity[indexName] !== null) { | ||
const relatedEntity = this.metadata.getEntityMetadataFromName(hasOneRelations[indexName].entity); | ||
value = entity[indexName][relatedEntity.primary]; | ||
if (property.indexed) { | ||
let value = entity[property.name]; | ||
if (hasOneRelations !== undefined && hasOneRelations[property.name] && entity[property.name] !== null) { | ||
const relatedEntity = this.metadata.getEntityMetadataFromName(hasOneRelations[property.name].entity); | ||
value = entity[property.name][relatedEntity.primary]; | ||
} | ||
if (value !== null) { | ||
if (property.type === 'Date' || property.type === 'Number') { | ||
await this.client.zadd( | ||
this.getIndexNumberKeyName(name, property.name), | ||
this.convertPropertyTypeToPrimitive(property, entity[property.name]), | ||
entity[primary], | ||
); | ||
} else { | ||
await this.client.zadd( | ||
this.getIndexKeyName(name, property.name, value), | ||
'0', | ||
entity[primary], | ||
); | ||
} | ||
} | ||
} | ||
if (value !== null) { | ||
await this.client.sadd(this.getIndexKeyName(name, indexName, value), entity[primary]); | ||
} | ||
} | ||
} | ||
await this.client.hmset(hashKey, valuesToStore); | ||
@@ -164,4 +164,13 @@ if (canBeListed && persistedEntity === null) { | ||
async list<T>(entityType: Type<T>, limit?: number, offset?: number, orderBy?: OrderBy): Promise<T[]> { | ||
const ids = await this.listIds(entityType, limit, offset, orderBy); | ||
async list<T>( | ||
entityType: Type<T>, | ||
where?: { | ||
conditions: WhereCondition[], | ||
type: 'AND' | 'OR', | ||
}, | ||
limit?: number, | ||
offset?: number, | ||
orderBy?: OrderBy, | ||
): Promise<T[]> { | ||
const ids = await this.listIds(entityType, where, limit, offset, orderBy); | ||
const response = []; | ||
@@ -176,28 +185,2 @@ | ||
async find<T>( | ||
entityType: Type<T>, | ||
conditions: Condition[], | ||
limit?: number, | ||
offset?: number, | ||
type: 'AND' | 'OR' = 'AND', | ||
): Promise<T[]> { | ||
const ids = await this.findIds(entityType, conditions, type); | ||
const response = []; | ||
if (limit !== undefined || offset !== undefined) { | ||
if (limit === undefined || offset === undefined) { | ||
throw new Error('You must specify limit and offset, not just one arg.'); | ||
} | ||
for (let index = offset; index < ids.length && index < (limit + offset); index++) { | ||
response.push(await this.getOne(entityType, ids[index])); | ||
} | ||
} else { | ||
for (const id of ids) { | ||
response.push(await this.getOne(entityType, id)); | ||
} | ||
} | ||
return response; | ||
} | ||
async search<T>(entityType: Type<T>, condition: Condition, limit: number): Promise<T[]> { | ||
@@ -241,26 +224,13 @@ const ids = await this.searchIds(entityType, condition, limit); | ||
async findIds<T>(entityType: Type<T>, conditions: Condition[], type: 'AND' | 'OR' = 'AND'): Promise<string[]> { | ||
if (conditions.length === 0) { | ||
throw new Error('You should at least specify one key to search'); | ||
} | ||
const { name } = this.metadata.getEntityMetadataFromType(entityType); | ||
const keyNames: string[] = []; | ||
for (const condition of conditions) { | ||
keyNames.push(this.getIndexKeyName(name, condition.key, String(condition.value))); | ||
} | ||
if (type === 'AND') { | ||
return await this.client.sinter(keyNames); | ||
} else { | ||
return await this.client.sunion(keyNames); | ||
} | ||
} | ||
async listIds<T>(entityType: Type<T>, limit?: number, offset?: number, orderBy?: OrderBy): Promise<string[]> { | ||
const { name, canBeListed } = this.metadata.getEntityMetadataFromType(entityType); | ||
async listIds<T>( | ||
entityType: Type<T>, | ||
where?: { | ||
conditions: WhereCondition[], | ||
type: 'AND' | 'OR', | ||
}, | ||
limit?: number, | ||
offset?: number, | ||
orderBy?: OrderBy, | ||
): Promise<string[]> { | ||
const { name, canBeListed, properties } = this.metadata.getEntityMetadataFromType(entityType); | ||
if (!canBeListed) { | ||
@@ -283,4 +253,4 @@ throw new Error(entityType.name + ' can\'t be listed!'); | ||
if (orderBy !== undefined) { | ||
const sortableKey = this.getSortableKeyName(name, orderBy.field); | ||
if (orderBy !== undefined && where === undefined) { | ||
const sortableKey = this.getIndexNumberKeyName(name, orderBy.field); | ||
@@ -295,2 +265,107 @@ | ||
if (where !== undefined) { | ||
if (where.conditions.length === 0) { | ||
throw new Error('Conditions can\'t be empty'); | ||
} | ||
const scores: {[key: string]: {min: any, max: any}} = {}; | ||
const equals: WhereCondition[] = []; | ||
for (const condition of where.conditions) { | ||
if (condition.comparator === '>') { | ||
if (!scores[condition.key]) { | ||
scores[condition.key] = {min: '-inf', max: '+inf'}; | ||
} | ||
scores[condition.key].min = this.convertPropertyTypeToPrimitive(properties[condition.key], condition.value); | ||
} | ||
if (condition.comparator === '<') { | ||
if (!scores[condition.key]) { | ||
scores[condition.key] = {min: '-inf', max: '+inf'}; | ||
} | ||
scores[condition.key].max = this.convertPropertyTypeToPrimitive(properties[condition.key], condition.value); | ||
} | ||
if (condition.comparator === '!=' || condition.comparator === '=') { | ||
if (properties[condition.key] === undefined || !properties[condition.key].indexed) { | ||
throw new Error('Property ' + condition.key + ' not found or not indexed'); | ||
} | ||
equals.push(condition); | ||
} | ||
} | ||
if (Object.keys(scores).length === 1 && equals.length === 0 && orderBy === undefined) { | ||
const scoreKey = Object.keys(scores)[0]; | ||
return await this.client.zrangebyscore( | ||
this.getIndexNumberKeyName(name, scoreKey), | ||
scores[scoreKey].min, | ||
scores[scoreKey].max, | ||
offset ? offset : 0, | ||
limit ? limit : -1, | ||
); | ||
} | ||
if (equals.length === 1 && Object.keys(scores).length === 0 && orderBy === undefined && equals[0].comparator != "!=") { | ||
const condition = equals[0]; | ||
return await this.client.zrange( | ||
this.getIndexKeyName(name, condition.key, this.convertPropertyTypeToPrimitive(properties[condition.key], condition.value)), | ||
start, | ||
stop, | ||
); | ||
} | ||
let luaOrderBy: { | ||
name: string, | ||
min: string, | ||
max: string, | ||
strategy: 'ASC' | 'DESC' | ||
} = undefined; | ||
if (orderBy !== undefined) { | ||
for (const scoreKey in scores) { | ||
if (scoreKey === orderBy.field) { | ||
luaOrderBy = { | ||
name: scoreKey, | ||
min: String(scores[scoreKey].min), | ||
max: String(scores[scoreKey].max), | ||
strategy: orderBy.strategy, | ||
} | ||
} | ||
} | ||
if (luaOrderBy === undefined) { | ||
luaOrderBy = { | ||
name: orderBy.field, | ||
strategy: orderBy.strategy, | ||
min: "-inf", | ||
max: "+inf", | ||
}; | ||
} | ||
} | ||
let luaArgs = { | ||
prefix: this.getIndexPrefix(name), | ||
listKey: this.getListKeyName(name), | ||
tempPrefix: 'temp:' + name + ':', | ||
orderBy: luaOrderBy, | ||
scores: Object.keys(scores).map((key: string) => ({ | ||
min: scores[key].min, | ||
max: scores[key].max, | ||
key, | ||
})), | ||
equals, | ||
limit: limit ? limit : -1, | ||
offset: offset ? offset : 0, | ||
type: where.type, | ||
}; | ||
return await this.client.eval( | ||
fs.readFileSync(path.join(__dirname, './lua/complex.query.lua')), | ||
0, | ||
JSON.stringify(luaArgs), | ||
); | ||
} | ||
return await this.client.lrange(keyName, start, stop); | ||
@@ -300,3 +375,3 @@ } | ||
async delete<T>(entityType: Type<T>, id: string): Promise<void> { | ||
const { name, uniques, indexes, canBeListed } = this.metadata.getEntityMetadataFromType(entityType); | ||
const { name, uniques, canBeListed } = this.metadata.getEntityMetadataFromType(entityType); | ||
const hashKey = name + ':' + id; | ||
@@ -308,5 +383,2 @@ | ||
} | ||
if (indexes) { | ||
await this.dropIndexes(persistedEntity, id); | ||
} | ||
@@ -317,3 +389,4 @@ if (canBeListed) { | ||
await this.dropSortables(persistedEntity); | ||
await this.dropIndexes(persistedEntity, id); | ||
await this.dropSearchables(persistedEntity); | ||
@@ -348,9 +421,10 @@ | ||
const result = await this.client.hmget(hashKey, properties.map((property: PropertyMetadata) => property.name)); | ||
const result = await this.client.hmget(hashKey, Object.keys(properties).map(key => properties[key].name)); | ||
const propertiesArray = Object.keys(properties).map(key => properties[key]); | ||
let index = 0; | ||
for (const resultKey of result) { | ||
if (hasOneRelations !== undefined && hasOneRelations[properties[index].name] && resultKey !== null) { | ||
entity[properties[index].name] = await this.getOne(hasOneRelations[properties[index].name].entityType as any, resultKey); | ||
if (hasOneRelations !== undefined && hasOneRelations[propertiesArray[index].name] && resultKey !== null) { | ||
entity[propertiesArray[index].name] = await this.getOne(hasOneRelations[propertiesArray[index].name].entityType as any, resultKey); | ||
} else { | ||
entity[properties[index].name] = this.convertStringToPropertyType(properties[index], resultKey); | ||
entity[propertiesArray[index].name] = this.convertStringToPropertyType(propertiesArray[index], resultKey); | ||
} | ||
@@ -398,19 +472,27 @@ index++; | ||
private async dropIndexes<T>(entity: T, id: string): Promise<void> { | ||
const { name, indexes, hasOneRelations } = this.metadata.getEntityMetadataFromInstance(entity); | ||
if (indexes) { | ||
for (const indexName of indexes) { | ||
let value = entity[indexName]; | ||
if (hasOneRelations !== undefined && hasOneRelations[indexName]) { | ||
const relatedEntity = this.metadata.getEntityMetadataFromName(hasOneRelations[indexName].entity); | ||
value = entity[indexName][relatedEntity.primary]; | ||
} | ||
await this.client.srem(this.getIndexKeyName(name, indexName, value), id); | ||
} | ||
const { properties } = this.metadata.getEntityMetadataFromInstance(entity); | ||
for (const property of Object.keys(properties).filter(key => properties[key].indexed).map(key => properties[key])) { | ||
await this.dropIndex(entity, property, id); | ||
} | ||
} | ||
private async dropIndex<T>(entity: T, property: PropertyMetadata, id: string): Promise<void> { | ||
const { name, hasOneRelations } = this.metadata.getEntityMetadataFromInstance(entity); | ||
let value = entity[property.name]; | ||
if (hasOneRelations !== undefined && hasOneRelations[property.name]) { | ||
const relatedEntity = this.metadata.getEntityMetadataFromName(hasOneRelations[property.name].entity); | ||
value = entity[property.name][relatedEntity.primary]; | ||
} | ||
if (property.type === 'Date' || property.type === 'Number') { | ||
await this.client.zrem(this.getIndexNumberKeyName(name, property.name), id); | ||
} else { | ||
await this.client.zrem(this.getIndexKeyName(name, property.name, value), id); | ||
} | ||
} | ||
private async dropSearchables<T>(entity: T): Promise<void> { | ||
const { name, properties, primary } = this.metadata.getEntityMetadataFromInstance(entity); | ||
for (const property of properties) { | ||
if (property.searchable === true) { | ||
for (const property of Object.keys(properties).map(key => properties[key])) { | ||
if (property.searchable) { | ||
await this.client.srem( | ||
@@ -424,18 +506,14 @@ this.getSearchableKeyName(name, property.name), | ||
private async dropSortables<T>(entity: T): Promise<void> { | ||
const { name, properties, primary } = this.metadata.getEntityMetadataFromInstance(entity); | ||
for (const property of properties) { | ||
if (property.sortable === true) { | ||
await this.client.zrem( | ||
this.getSortableKeyName(name, property.name), | ||
entity[primary], | ||
); | ||
} | ||
} | ||
private getIndexNumberKeyName(entityName: string, indexName: string): string { | ||
return this.getIndexPrefix(entityName) + indexName; | ||
} | ||
private getIndexKeyName(entityName: string, indexName: string, indexValue: string): string { | ||
return entityName + ':index:' + indexName + ':' + indexValue; | ||
return this.getIndexPrefix(entityName) + indexName + ':' + indexValue; | ||
} | ||
private getIndexPrefix(entityName: string): string { | ||
return entityName + ':index:'; | ||
} | ||
private getListKeyName(entityName: string): string { | ||
@@ -449,6 +527,2 @@ return entityName + ':list'; | ||
private getSortableKeyName(entityName: string, fieldName: string): string { | ||
return entityName + ':sort:' + fieldName; | ||
} | ||
private getSearchableKeyName(entityName: string, fieldName: string): string { | ||
@@ -455,0 +529,0 @@ return entityName + ':search:' + fieldName; |
import { RediskTestUtils } from './utils/redisk-test-utils'; | ||
import { users } from './fixtures/users'; | ||
import { User } from './models/user.model'; | ||
import { User } from './entities/user.entity'; | ||
@@ -5,0 +5,0 @@ let utils: RediskTestUtils; |
import { RediskTestUtils } from './utils/redisk-test-utils'; | ||
import { users } from './fixtures/users'; | ||
import { User } from './models/user.model'; | ||
import { User } from './entities/user.entity'; | ||
import { groups } from './fixtures/groups'; | ||
@@ -36,7 +36,7 @@ | ||
expect(await utils.redisk.getClient().sinter('user:index:color:' + users[0].color)).toEqual([]); | ||
expect(await utils.redisk.getClient().sinter('user:index:food:' + users[0].food)).toEqual([ users[2].id ]); | ||
expect(await utils.redisk.getClient().sinter('user:index:group:' + groups[0].id)).toEqual([ users[1].id ]); | ||
expect(await utils.redisk.getClient().zrange('user:index:color:' + users[0].color, 0, -1)).toEqual([]); | ||
expect(await utils.redisk.getClient().zrange('user:index:food:' + users[0].food, 0, -1)).toEqual([ users[2].id ]); | ||
expect(await utils.redisk.getClient().zrange('user:index:group:' + groups[0].id, 0, -1)).toEqual([ users[1].id ]); | ||
expect(await utils.redisk.getClient().zrange('user:sort:created', 0, -1)).toEqual([ users[1].id, users[2].id ]); | ||
expect(await utils.redisk.getClient().zrange('user:index:created', 0, -1)).toEqual([ users[1].id, users[2].id ]); | ||
@@ -43,0 +43,0 @@ expect(await utils.redisk.getClient().smembers('user:search:name')).toEqual( |
@@ -1,2 +0,2 @@ | ||
import { Group } from '../models/group.model'; | ||
import { Group } from '../entities/group.entity'; | ||
@@ -3,0 +3,0 @@ export const groups = [ |
@@ -1,2 +0,2 @@ | ||
import { User } from '../models/user.model'; | ||
import { User } from '../entities/user.entity'; | ||
import { groups } from './groups'; | ||
@@ -3,0 +3,0 @@ |
import { RediskTestUtils } from './utils/redisk-test-utils'; | ||
import { users } from './fixtures/users'; | ||
import { User } from './models/user.model'; | ||
import { User } from './entities/user.entity'; | ||
@@ -5,0 +5,0 @@ let utils: RediskTestUtils; |
import { RediskTestUtils } from './utils/redisk-test-utils'; | ||
import { users } from './fixtures/users'; | ||
import { User } from './models/user.model'; | ||
import { User } from './entities/user.entity'; | ||
import { groups } from './fixtures/groups'; | ||
@@ -30,3 +31,3 @@ let utils: RediskTestUtils; | ||
it('should return all persisted entities', async () => { | ||
expect((await utils.redisk.list(User)).sort()).toEqual(users.sort()); | ||
expect((await utils.redisk.list(User))).toEqual(users); | ||
}); | ||
@@ -40,3 +41,3 @@ }); | ||
const offset = 1; | ||
expect((await utils.redisk.list(User, limit, offset)).sort()).toEqual([users[1], users[2]].sort()); | ||
expect((await utils.redisk.list(User, undefined, limit, offset)).sort()).toEqual([users[1], users[2]].sort()); | ||
}); | ||
@@ -47,3 +48,3 @@ }); | ||
it('should return persisted entities sorted', async () => { | ||
const response = await utils.redisk.list(User, undefined, undefined, { field: 'created', strategy: 'ASC' }); | ||
const response = await utils.redisk.list(User, undefined, undefined, undefined, { field: 'created', strategy: 'ASC' }); | ||
expect(response).toEqual(users); | ||
@@ -55,3 +56,3 @@ }); | ||
it('should return persisted entities sorted', async () => { | ||
const response = await utils.redisk.list(User, 2, 1, { field: 'created', strategy: 'ASC' }); | ||
const response = await utils.redisk.list(User, undefined, 2, 1, { field: 'created', strategy: 'ASC' }); | ||
expect(response).toEqual([users[1], users[2]]); | ||
@@ -63,5 +64,304 @@ }); | ||
it('should return persisted entities sorted', async () => { | ||
const response = await utils.redisk.list(User, undefined, undefined, { field: 'created', strategy: 'DESC' }); | ||
expect(response).toEqual(users.reverse()); | ||
const response = await utils.redisk.list(User, undefined, undefined, undefined, { field: 'created', strategy: 'DESC' }); | ||
expect(response).toEqual([users[4], users[3], users[2], users[1], users[0]]); | ||
}); | ||
}); | ||
describe('List with one condition', () => { | ||
it('should return filtered persisted entities', async () => { | ||
expect((await utils.redisk.list( | ||
User, | ||
{ | ||
conditions: [ | ||
{ | ||
key: 'color', | ||
value: 'red', | ||
comparator: '=', | ||
}, | ||
], | ||
type: 'AND', | ||
}, | ||
))).toEqual([users[0], users[4]]); | ||
}); | ||
}); | ||
describe('List with relation index', () => { | ||
it('should return filtered persisted entities', async () => { | ||
expect((await utils.redisk.list( | ||
User, | ||
{ | ||
conditions: [ | ||
{ | ||
key: 'group', | ||
value: groups[0].id, | ||
comparator: '=', | ||
} | ||
], | ||
type: 'AND', | ||
}, | ||
))).toEqual([users[0], users[1]]); | ||
}); | ||
}); | ||
describe('List with one condition with limit and offset', () => { | ||
it('should return filtered persisted entities', async () => { | ||
expect(await utils.redisk.list( | ||
User, | ||
{ | ||
conditions: [ | ||
{ | ||
key: 'color', | ||
value: 'blue', | ||
comparator: '=', | ||
} | ||
], | ||
type: 'AND', | ||
}, | ||
1, | ||
2, | ||
)).toEqual([users[1]]); | ||
}); | ||
}); | ||
describe('List with two AND condition and order by', () => { | ||
it('should return filtered persisted entities', async () => { | ||
expect((await utils.redisk.list( | ||
User, | ||
{ | ||
conditions: [ | ||
{ | ||
key: 'color', | ||
value: 'blue', | ||
comparator: '=', | ||
}, | ||
{ | ||
key: 'food', | ||
value: 'tofu', | ||
comparator: '=', | ||
}, | ||
], | ||
type: 'AND' | ||
}, | ||
undefined, | ||
undefined, | ||
{ | ||
field: 'created', | ||
strategy: 'DESC', | ||
}, | ||
))).toEqual([users[3], users[2]]); | ||
}); | ||
}); | ||
describe('List with two AND condition', () => { | ||
it('should return filtered persisted entities', async () => { | ||
expect((await utils.redisk.list( | ||
User, | ||
{ | ||
conditions: [ | ||
{ | ||
key: 'color', | ||
value: 'blue', | ||
comparator: '=', | ||
}, | ||
{ | ||
key: 'food', | ||
value: 'tofu', | ||
comparator: '=', | ||
}, | ||
], | ||
type: 'AND' | ||
}, | ||
))).toEqual([users[2], users[3]]); | ||
}); | ||
}); | ||
describe('List with two OR condition', () => { | ||
it('should return filtered persisted entities', async () => { | ||
expect((await utils.redisk.list( | ||
User, | ||
{ | ||
conditions: [ | ||
{ | ||
key: 'color', | ||
value: 'red', | ||
comparator: '=', | ||
}, | ||
{ | ||
key: 'food', | ||
value: 'avocado', | ||
comparator: '=' | ||
}, | ||
], | ||
type: 'OR', | ||
}, | ||
))).toEqual([users[4], users[0], users[1]]); | ||
}); | ||
}); | ||
describe('List with greater than condition', () => { | ||
it('should return filteres entities', async () => { | ||
expect(await utils.redisk.list( | ||
User, | ||
{ | ||
conditions: [ | ||
{ | ||
key: 'created', | ||
value: new Date('2020-02-23 20:35:00'), | ||
comparator: '>' | ||
}, | ||
], | ||
type: 'AND' | ||
} | ||
)).toEqual([users[3], users[4]]); | ||
}); | ||
}); | ||
describe('List with greater than and equal condition', () => { | ||
it('should return filteres entities', async () => { | ||
expect(await utils.redisk.list( | ||
User, | ||
{ | ||
conditions: [ | ||
{ | ||
key: 'created', | ||
value: new Date('2020-02-23 20:35:00'), | ||
comparator: '>' | ||
}, | ||
{ | ||
key: 'color', | ||
value: 'blue', | ||
comparator: '=' | ||
}, | ||
], | ||
type: 'AND' | ||
} | ||
)).toEqual([users[3]]); | ||
}); | ||
}); | ||
describe('List with greater than condition with limit and offset', () => { | ||
it('should return filteres entities', async () => { | ||
expect(await utils.redisk.list( | ||
User, | ||
{ | ||
conditions: [ | ||
{ | ||
key: 'created', | ||
value: new Date('2020-02-23 20:35:00'), | ||
comparator: '>' | ||
}, | ||
], | ||
type: 'AND' | ||
}, | ||
1, | ||
1, | ||
)).toEqual([users[4]]); | ||
}); | ||
}); | ||
describe('List with less than condition', () => { | ||
it('should return filteres entities', async () => { | ||
expect(await utils.redisk.list( | ||
User, | ||
{ | ||
conditions: [ | ||
{ | ||
key: 'created', | ||
value: new Date('2020-02-22 10:34:00'), | ||
comparator: '<' | ||
}, | ||
], | ||
type: 'AND' | ||
} | ||
)).toEqual([users[0]]); | ||
}); | ||
}); | ||
describe('List with less than and greater than condition', () => { | ||
it('should return filteres entities', async () => { | ||
expect(await utils.redisk.list( | ||
User, | ||
{ | ||
conditions: [ | ||
{ | ||
key: 'created', | ||
value: new Date('2020-02-22 10:34:00'), | ||
comparator: '>' | ||
}, | ||
{ | ||
key: 'created', | ||
value: new Date('2020-02-22 13:10:00'), | ||
comparator: '<' | ||
}, | ||
], | ||
type: 'AND' | ||
} | ||
)).toEqual([users[1]]); | ||
}); | ||
}); | ||
describe('List with order by and condition', () => { | ||
it('should return filteres entities', async () => { | ||
expect(await utils.redisk.list( | ||
User, | ||
{ | ||
conditions: [ | ||
{ | ||
key: 'food', | ||
value: 'tofu', | ||
comparator: '=' | ||
}, | ||
], | ||
type: 'AND' | ||
}, | ||
1, | ||
0, | ||
{ | ||
field: 'created', | ||
strategy: 'DESC' | ||
}, | ||
)).toEqual([users[3]]); | ||
}); | ||
}); | ||
describe('List with order by and condition not equal', () => { | ||
it('should return filteres entities', async () => { | ||
expect(await utils.redisk.list( | ||
User, | ||
{ | ||
conditions: [ | ||
{ | ||
key: 'food', | ||
value: 'tofu', | ||
comparator: '!=' | ||
}, | ||
], | ||
type: 'AND' | ||
}, | ||
1, | ||
0, | ||
{ | ||
field: 'created', | ||
strategy: 'DESC' | ||
}, | ||
)).toEqual([users[4]]); | ||
}); | ||
}); | ||
describe('List with condition not equal', () => { | ||
it('should return filteres entities', async () => { | ||
expect(await utils.redisk.list( | ||
User, | ||
{ | ||
conditions: [ | ||
{ | ||
key: 'food', | ||
value: 'tofu', | ||
comparator: '!=' | ||
}, | ||
], | ||
type: 'AND' | ||
}, | ||
)).toEqual([users[4], users[1]]); | ||
}); | ||
}); |
import { RediskTestUtils } from './utils/redisk-test-utils'; | ||
import { users } from './fixtures/users'; | ||
import { User } from './models/user.model'; | ||
import { Group } from './models/group.model'; | ||
import { User } from './entities/user.entity'; | ||
import { Group } from './entities/group.entity'; | ||
@@ -59,6 +59,6 @@ let utils: RediskTestUtils; | ||
expect(await utils.redisk.getClient().sinter('user:index:color:' + color)).toEqual([ users[0].id, id ]); | ||
expect(await utils.redisk.getClient().sinter('user:index:food:' + food)).toEqual([ id ]); | ||
expect(await utils.redisk.getClient().zrange('user:index:color:' + color, 0, -1)).toEqual([ users[0].id, id ]); | ||
expect(await utils.redisk.getClient().zrange('user:index:food:' + food, 0, -1)).toEqual([ id ]); | ||
expect(await utils.redisk.getClient().zrange('user:sort:created', 0, -1)).toEqual([ id, users[0].id ]); | ||
expect(await utils.redisk.getClient().zrange('user:index:created', 0, -1)).toEqual([ id, users[0].id ]); | ||
@@ -122,7 +122,7 @@ expect(await utils.redisk.getClient().smembers('user:search:name')).toEqual( | ||
expect(await utils.redisk.getClient().sinter('user:index:color:' + color)).toEqual([ users[0].id ]); | ||
expect(await utils.redisk.getClient().sinter('user:index:color:' + newColor)).toEqual([ id ]); | ||
expect(await utils.redisk.getClient().sinter('user:index:food:' + food)).toEqual([ id ]); | ||
expect(await utils.redisk.getClient().zrange('user:index:color:' + color, 0, -1)).toEqual([ users[0].id ]); | ||
expect(await utils.redisk.getClient().zrange('user:index:color:' + newColor, 0, -1)).toEqual([ id ]); | ||
expect(await utils.redisk.getClient().zrange('user:index:food:' + food, 0, -1)).toEqual([ id ]); | ||
expect(await utils.redisk.getClient().zrange('user:sort:created', 0, -1)).toEqual([ users[0].id, id ]); | ||
expect(await utils.redisk.getClient().zrange('user:index:created', 0, -1)).toEqual([ users[0].id, id ]); | ||
@@ -161,7 +161,7 @@ expect(await utils.redisk.getClient().smembers('user:search:name')).toEqual( | ||
expect(await utils.redisk.getClient().sinter('user:index:color:' + color)).toEqual([ users[0].id ]); | ||
expect(await utils.redisk.getClient().sinter('user:index:color:' + newColor)).toEqual([ id ]); | ||
expect(await utils.redisk.getClient().sinter('user:index:food:' + food)).toEqual([ id ]); | ||
expect(await utils.redisk.getClient().zrange('user:index:color:' + color, 0, -1)).toEqual([ users[0].id ]); | ||
expect(await utils.redisk.getClient().zrange('user:index:color:' + newColor, 0, -1)).toEqual([ id ]); | ||
expect(await utils.redisk.getClient().zrange('user:index:food:' + food, 0, -1)).toEqual([ id ]); | ||
expect(await utils.redisk.getClient().zrange('user:sort:created', 0, -1)).toEqual([ users[0].id, id ]); | ||
expect(await utils.redisk.getClient().zrange('user:index:created', 0, -1)).toEqual([ users[0].id, id ]); | ||
@@ -168,0 +168,0 @@ expect(await utils.redisk.getClient().smembers('user:search:name')).toEqual( |
import { RediskTestUtils } from './utils/redisk-test-utils'; | ||
import { users } from './fixtures/users'; | ||
import { User } from './models/user.model'; | ||
import { User } from './entities/user.entity'; | ||
@@ -5,0 +5,0 @@ let utils: RediskTestUtils; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
187491
85
2366
380
6
1