-
Notifications
You must be signed in to change notification settings - Fork 81
/
Copy pathrepository.ts
262 lines (228 loc) · 8.56 KB
/
repository.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
import Schema from "../schema/schema"
import Client from "../client";
import Entity from "../entity/entity";
import { Search, RawSearch } from '../search/search';
import { EntityData } from "../entity/entity";
import { Point } from "../schema/schema-definitions";
import { CreateIndexOptions } from "../client";
import { JsonConverter, HashConverter } from "./converter";
/**
* Initialization data for {@link Entity} creation when calling
* {@link Repository.createEntity} or {@link Repository.createAndSave}.
*/
export type EntityCreationData = Record<string, number | boolean | string | string[] | Point | Date | null>;
/**
* A repository is the main interaction point for reading, writing, and
* removing {@link Entity | Entities} from Redis. Create one by calling
* {@link Client.fetchRepository} and passing in a {@link Schema}. Then
* use the {@link Repository.fetch}, {@link Repository.save}, and
* {@link Repository.remove} methods to manage your data:
*
* ```typescript
* let repository = client.fetchRepository<Foo>(schema);
*
* let foo = await repository.fetch('01FK6TCJBDK41RJ766A4SBWDJ9');
* foo.aString = 'bar';
* foo.aBoolean = false;
* await repository.save(foo);
* ```
*
* Be sure to use the repository to create a new instance of an
* {@link Entity} you want to create before you save it:
* ```typescript
* let foo = await repository.createEntity();
* foo.aString = 'bar';
* foo.aBoolean = false;
* await repository.save(foo);
* ```
*
* If you want to the {@link Repository.search} method, you need to create an index
* first, and you need RediSearch or RedisJSON installed on your instance of Redis:
*
* ```typescript
* await repository.createIndex();
* let entities = await repository.search()
* .where('aString').eq('bar')
* .and('aBoolean').is.false().returnAll();
* ```
*
* @template TEntity The type of {@link Entity} that this repository manages.
*/
export default abstract class Repository<TEntity extends Entity> {
protected client: Client;
private schema: Schema<TEntity>;
/** @internal */
constructor(schema: Schema<TEntity>, client: Client) {
this.schema = schema;
this.client = client
}
/**
* Creates an index in Redis for use by the {@link Repository.search} method. Requires
* that RediSearch or RedisJSON is installed on your instance of Redis.
*/
async createIndex() {
let currentIndexHash = await this.client.get(this.schema.indexHashName)
if (currentIndexHash !== this.schema.indexHash) {
await this.dropIndex();
let options : CreateIndexOptions = {
indexName: this.schema.indexName,
dataStructure: this.schema.dataStructure,
prefix: `${this.schema.prefix}:`,
schema: this.schema.redisSchema
};
if (this.schema.useStopWords === 'OFF') options.stopWords = []
if (this.schema.useStopWords === 'CUSTOM') options.stopWords = this.schema.stopWords
await this.client.createIndex(options);
await this.client.set(this.schema.indexHashName, this.schema.indexHash);
}
}
/**
* Removes an existing index from Redis. Use this method if you want to swap out your index
* because your {@link Entity} has changed. Requires that RediSearch or RedisJSON is installed
* on your instance of Redis.
*/
async dropIndex() {
try {
await this.client.unlink(this.schema.indexHashName);
await this.client.dropIndex(this.schema.indexName);
} catch (e) {
if (e instanceof Error && e.message === "Unknown Index name") {
// no-op: the thing we are dropping doesn't exist
} else {
throw e
}
}
}
/**
* Creates an {@link Entity} with a populated {@link Entity.entityId} property.
* @param data Optional values with which to initialize the entity.
* @returns A newly created Entity.
*/
createEntity(data: EntityCreationData = {}): TEntity {
let id = this.schema.generateId();
let entity = new this.schema.entityCtor(this.schema, id);
for (let key in data) {
if (this.schema.entityCtor.prototype.hasOwnProperty(key)) {
(entity as Record<string, any>)[key] = data[key]
}
}
return entity;
}
/**
* Save the {@link Entity} to Redis. If it already exists, it will be updated. If it doesn't
* exist, it will be created.
* @param entity The Entity to save.
* @returns The ID of the Entity just saved.
*/
async save(entity: TEntity) : Promise<string> {
let key = this.makeKey(entity.entityId);
if (Object.keys(entity.entityData).length === 0) {
await this.client.unlink(key);
} else {
await this.writeEntity(key, entity.entityData);
}
return entity.entityId;
}
/**
* Creates and saves an {@link Entity}. Equivalent of calling
* {@link Repository.createEntity} followed by {@link Repository.save}.
* @param data Optional values with which to initialize the entity.
* @returns The newly created and saved Entity.
*/
async createAndSave(data: EntityCreationData = {}): Promise<TEntity> {
let entity = this.createEntity(data);
await this.save(entity)
return entity
}
/**
* Read and return an {@link Entity} from Redis with the given id. If
* the {@link Entity} is not found, returns an {@link Entity} with all
* properties set to `null`.
* @param id The ID of the {@link Entity} you seek.
* @returns The matching Entity.
*/
async fetch(id: string): Promise<TEntity> {
let key = this.makeKey(id);
let entityData = await this.readEntity(key);
return new this.schema.entityCtor(this.schema, id, entityData);
}
/**
* Remove an {@link Entity} from Redis with the given id. If the {@link Entity} is
* not found, does nothing.
* @param id The ID of the {@link Entity} you with to delete.
*/
async remove(id: string): Promise<void> {
let key = this.makeKey(id);
await this.client.unlink(key);
}
/**
* Set the time to live of the {@link Entity}. If the {@link Entity} is not
* found, does nothing.
* @param id The ID of the {@link Entity} to set and expiration for.
* @param ttlInSeconds THe time to live in seconds.
*/
async expire(id: string, ttlInSeconds: number) {
let key = this.makeKey(id);
await this.client.expire(key, ttlInSeconds);
}
/**
* Kicks off the process of building a query. Requires that RediSearch (and optionally
* RedisJSON) be is installed on your instance of Redis.
* @template TEntity The type of {@link Entity} sought.
* @returns A {@link Search} object.
*/
search(): Search<TEntity> {
return new Search<TEntity>(this.schema, this.client);
}
/**
* Creates a search that bypassed Redis OM and instead allows you to execute a raw
* RediSearch query. Requires that RediSearch (and optionally RedisJSON) be installed
* on your instance of Redis.
* @template TEntity The type of {@link Entity} sought.
* @query The raw RediSearch query you want to rune.
* @returns A {@link RawSearch} object.
*/
searchRaw(query: string): RawSearch<TEntity> {
return new RawSearch<TEntity>(this.schema, this.client, query);
}
/** @internal */
protected abstract writeEntity(key: string, data: EntityData): Promise<void>;
/** @internal */
protected abstract readEntity(key: string): Promise<EntityData>;
/** @internal */
protected makeKey(id: string): string {
return `${this.schema.prefix}:${id}`;
}
}
/** @internal */
export class HashRepository<TEntity extends Entity> extends Repository<TEntity> {
private converter: HashConverter;
constructor(schema: Schema<TEntity>, client: Client) {
super(schema, client);
this.converter = new HashConverter(schema.definition);
}
protected async writeEntity(key: string, data: EntityData): Promise<void> {
let hashData = this.converter.toHashData(data);
await this.client.hsetall(key, hashData);
}
protected async readEntity(key: string): Promise<EntityData> {
let hashData = await this.client.hgetall(key);
return this.converter.toEntityData(hashData);
}
}
/** @internal */
export class JsonRepository<TEntity extends Entity> extends Repository<TEntity> {
private converter: JsonConverter;
constructor(schema: Schema<TEntity>, client: Client) {
super(schema, client);
this.converter = new JsonConverter(schema.definition);
}
protected async writeEntity(key: string, data: EntityData): Promise<void> {
let jsonData = this.converter.toJsonData(data);
await this.client.jsonset(key, jsonData);
}
protected async readEntity(key: string): Promise<EntityData> {
let jsonData = await this.client.jsonget(key);
return this.converter.toEntityData(jsonData);
}
}