-
Notifications
You must be signed in to change notification settings - Fork 81
/
Copy pathindex.ts
297 lines (259 loc) · 9.63 KB
/
index.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
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
import { Schema } from "../schema/schema"
import { Client } from "../client";
import { Entity } from "../entity/entity";
import { Search, RawSearch } from '../search/search';
import { CreateIndexOptions } from "../client";
import { EntityData } from "../entity/entity-data";
/**
* 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
* const repository = client.fetchRepository<Foo>(schema);
*
* const 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
* const 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();
* const 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 abstract class Repository<TEntity extends Entity> {
protected client: Client;
protected 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() {
const currentIndexHash = await this.client.get(this.schema.indexHashName)
if (currentIndexHash !== this.schema.indexHash) {
await this.dropIndex();
const 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: EntityData = {}): TEntity {
const id = this.schema.generateId();
return new this.schema.entityCtor(this.schema, id, data);
}
/**
* 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> {
await this.writeEntity(entity);
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: EntityData = {}): Promise<TEntity> {
const 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>
/**
* Read and return the {@link Entity | Entities} from Redis with the given IDs. If
* a particular {@link Entity} is not found, returns an {@link Entity} with all
* properties set to `null`.
* @param ids The IDs of the {@link Entity | Entities} you seek.
* @returns The matching Entities.
*/
async fetch(...ids: string[]): Promise<TEntity[]>
/**
* Read and return the {@link Entity | Entities} from Redis with the given IDs. If
* a particular {@link Entity} is not found, returns an {@link Entity} with all
* properties set to `null`.
* @param ids The IDs of the {@link Entity | Entities} you seek.
* @returns The matching Entities.
*/
async fetch(ids: string[]): Promise<TEntity[]>
async fetch(ids: string | string[]): Promise<TEntity | TEntity[]> {
if (arguments.length > 1) {
return this.readEntities([...arguments]);
}
if (Array.isArray(ids)) {
return this.readEntities(ids)
}
const entities = await this.readEntities([ids])
return entities[0]
}
/**
* 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 wish to delete.
*/
async remove(id: string): Promise<void>
/**
* Remove the {@link Entity | Entities} from Redis with the given ids. If a
* particular {@link Entity} is not found, does nothing.
* @param ids The IDs of the {@link Entity | Entities} you wish to delete.
*/
async remove(...ids: string[]): Promise<void>
/**
* Remove the {@link Entity | Entities} from Redis with the given ids. If a
* particular {@link Entity} is not found, does nothing.
* @param ids The IDs of the {@link Entity | Entities} you wish to delete.
*/
async remove(ids: string[]): Promise<void>
async remove(ids: string | string[]): Promise<void> {
const keys = arguments.length > 1
? this.makeKeys([...arguments])
: Array.isArray(ids)
? this.makeKeys(ids)
: ids ? this.makeKeys([ids]) : []
if (keys.length === 0) return;
await this.client.unlink(...keys);
}
/**
* 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) {
const 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(entity: TEntity): Promise<void>;
/** @internal */
protected abstract readEntities(ids: string[]): Promise<TEntity[]>;
/** @internal */
protected makeKeys(ids: string[]): string[] {
return ids.map(id => this.makeKey(id));
}
/** @internal */
protected makeKey(id: string): string {
return `${this.schema.prefix}:${id}`;
}
}
/** @internal */
export class HashRepository<TEntity extends Entity> extends Repository<TEntity> {
protected async writeEntity(entity: TEntity): Promise<void> {
const data = entity.toRedisHash();
if (Object.keys(data).length === 0) {
await this.client.unlink(entity.keyName);
return;
}
await this.client.hsetall(entity.keyName, data);
}
protected async readEntities(ids: string[]): Promise<TEntity[]> {
return Promise.all(
ids.map(async (id) => {
const key = this.makeKey(id);
const hashData = await this.client.hgetall(key);
const entity = new this.schema.entityCtor(this.schema, id);
entity.fromRedisHash(hashData);
return entity;
}));
}
}
/** @internal */
export class JsonRepository<TEntity extends Entity> extends Repository<TEntity> {
protected async writeEntity(entity: TEntity): Promise<void> {
await this.client.jsonset(entity.keyName, entity.toRedisJson());
}
protected async readEntities(ids: string[]): Promise<TEntity[]> {
return Promise.all(
ids.map(async (id) => {
const key = this.makeKey(id);
const jsonData = await this.client.jsonget(key);
const entity = new this.schema.entityCtor(this.schema, id);
entity.fromRedisJson(jsonData);
return entity;
}));
}
}