@desmat/redis-store 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +5 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +274 -0
- package/package.json +36 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Mathieu Desjarlais
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Redis } from "@upstash/redis";
|
|
2
|
+
export type RedisStoreRecord = {
|
|
3
|
+
id: string;
|
|
4
|
+
createdAt: number;
|
|
5
|
+
updatedAt?: number;
|
|
6
|
+
deletedAt?: number;
|
|
7
|
+
};
|
|
8
|
+
export declare class RedisStore<T extends RedisStoreRecord> {
|
|
9
|
+
redis: Redis;
|
|
10
|
+
key: string;
|
|
11
|
+
setKey: string;
|
|
12
|
+
valueKey: (id: string) => string;
|
|
13
|
+
recordOptions: any;
|
|
14
|
+
debug: boolean;
|
|
15
|
+
constructor({ url, token, key, setKey, recordOptions, debug, }: {
|
|
16
|
+
url: string;
|
|
17
|
+
token: string;
|
|
18
|
+
key: string;
|
|
19
|
+
setKey?: string;
|
|
20
|
+
recordOptions?: any;
|
|
21
|
+
debug?: boolean;
|
|
22
|
+
});
|
|
23
|
+
lookupKeys(value: any, options?: any): any[][];
|
|
24
|
+
exists(id: string): Promise<boolean>;
|
|
25
|
+
get(id: string): Promise<T | undefined>;
|
|
26
|
+
scan(query?: any): Promise<Set<string>>;
|
|
27
|
+
ids(query?: any): Promise<Set<string>>;
|
|
28
|
+
find(query?: any): Promise<T[]>;
|
|
29
|
+
create(value: any, options?: any): Promise<T>;
|
|
30
|
+
update(value: any, options?: any): Promise<T>;
|
|
31
|
+
delete(id: string, options?: any): Promise<T>;
|
|
32
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
Some useful commands
|
|
4
|
+
|
|
5
|
+
keys *
|
|
6
|
+
scan 0 match thing:*
|
|
7
|
+
del thing1 thing2 etc
|
|
8
|
+
json.get things $
|
|
9
|
+
json.get things '$[?((@.deletedAt > 0) == false)]'
|
|
10
|
+
json.get things '$[?((@.deletedAt > 0) == true)]'
|
|
11
|
+
json.get things '$[?(@.createdBy == "UID")]'
|
|
12
|
+
json.get things '$[?(@.content ~= "(?i)lorem")]'
|
|
13
|
+
json.get things '$[?(@.id ~= "(ID1)|(ID2)")]
|
|
14
|
+
json.set thing:UUID '$.foos[5].bar' '{"car": 42}'
|
|
15
|
+
json.set thing:UUID '$.foos[1].bar.car' '42'
|
|
16
|
+
json.get userhaikus '$[?(@.haikuId == "ID" && (@.likedAt > 0) == true)]'
|
|
17
|
+
*/
|
|
18
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
19
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
20
|
+
};
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.RedisStore = void 0;
|
|
23
|
+
const moment_1 = __importDefault(require("moment"));
|
|
24
|
+
const utils_1 = require("@desmat/utils");
|
|
25
|
+
const redis_1 = require("@upstash/redis");
|
|
26
|
+
class RedisStore {
|
|
27
|
+
constructor({ url, token, key, setKey, recordOptions, debug, }) {
|
|
28
|
+
this.redis = new redis_1.Redis({ url, token });
|
|
29
|
+
this.key = key;
|
|
30
|
+
this.setKey = setKey || key + "s";
|
|
31
|
+
this.valueKey = (id) => `${key}:${id}`;
|
|
32
|
+
this.recordOptions = recordOptions;
|
|
33
|
+
this.debug = !!debug;
|
|
34
|
+
}
|
|
35
|
+
lookupKeys(value, options) {
|
|
36
|
+
options = { ...this.recordOptions, ...options };
|
|
37
|
+
this.debug && console.log(`RedisStore.lookupKeys<${this.key}>.lookupKeys`, { value, options });
|
|
38
|
+
/*
|
|
39
|
+
create index and lookup sets based on options.lookups
|
|
40
|
+
|
|
41
|
+
given a likedhaiku record:
|
|
42
|
+
{
|
|
43
|
+
id: 123:456,
|
|
44
|
+
userId: 123,
|
|
45
|
+
haikuId 456,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
and lookups:
|
|
49
|
+
{
|
|
50
|
+
user: { userId: "haikuId"},
|
|
51
|
+
haiku: { haikuId: "userId" }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
we want indexes:
|
|
55
|
+
|
|
56
|
+
likedhaiku:123:456 -> value (JSON, the rest are sorted sets, not handled here)
|
|
57
|
+
likedhaikus -> all likedhaiku id's (ie 123:456, etc, not handled here)
|
|
58
|
+
// NOT SUPPORTED FOR NOW // likedhaikus:users -> all user ids (ie 123, etc) NOTE: this should be a sorted set of user ids with its score as number of haikus liked
|
|
59
|
+
likedhaikus:user:123 -> all likedhaiku id's for the given user (ie 123:456, etc)
|
|
60
|
+
// NOT SUPPORTED FOR NOW // likedhaikus:haikus -> NOTE: this should be a sorted set of haiku ids with its score as number of users who liked it
|
|
61
|
+
likedhaikus:haiku:456 -> all likedhaiku id's for the given haiku (ie 123:456, etc)
|
|
62
|
+
|
|
63
|
+
*/
|
|
64
|
+
const lookupKeys = !(options === null || options === void 0 ? void 0 : options.noLookup) && Object
|
|
65
|
+
.entries((options === null || options === void 0 ? void 0 : options.lookups) || {})
|
|
66
|
+
.map((entry) => {
|
|
67
|
+
const id = value.id;
|
|
68
|
+
const lookupName = entry[0];
|
|
69
|
+
const lookupKey = entry[1];
|
|
70
|
+
// TODO validate and log errors
|
|
71
|
+
// @ts-ignore
|
|
72
|
+
const lookupId = value[lookupKey];
|
|
73
|
+
// foos:bar:123 -> 123:456
|
|
74
|
+
return [`${this.setKey}:${lookupName}:${lookupId}`, id];
|
|
75
|
+
}) || [];
|
|
76
|
+
this.debug && console.log(`RedisStore<${this.key}>.lookupKeys`, { options, lookupKeys });
|
|
77
|
+
return lookupKeys;
|
|
78
|
+
}
|
|
79
|
+
async exists(id) {
|
|
80
|
+
this.debug && console.log(`RedisStore<${this.key}>.exists`, { id });
|
|
81
|
+
const response = await this.redis.exists(this.valueKey(id));
|
|
82
|
+
this.debug && console.log(`RedisStore<${this.key}>.exists`, { response });
|
|
83
|
+
return response > 0;
|
|
84
|
+
}
|
|
85
|
+
async get(id) {
|
|
86
|
+
this.debug && console.log(`RedisStore<${this.key}>.get`, { id });
|
|
87
|
+
const response = await this.redis.json.get(this.valueKey(id), "$");
|
|
88
|
+
this.debug && console.log(`RedisStore<${this.key}>.get`, { response });
|
|
89
|
+
let value;
|
|
90
|
+
if (response && response[0] && !response[0].deletedAt) {
|
|
91
|
+
value = response[0];
|
|
92
|
+
}
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
async scan(query = {}) {
|
|
96
|
+
this.debug && console.log(`RedisStore<${this.key}>.scan`, { query });
|
|
97
|
+
const count = query.count;
|
|
98
|
+
const match = this.valueKey(query.scan);
|
|
99
|
+
let keys = new Set();
|
|
100
|
+
let nextCursor = "0";
|
|
101
|
+
do {
|
|
102
|
+
const ret = await this.redis.scan(nextCursor, { match, type: "json", count: count - keys.size });
|
|
103
|
+
this.debug && console.log(`RedisStore<${this.key}>.scan`, { ret });
|
|
104
|
+
nextCursor = ret[0];
|
|
105
|
+
ret[1].forEach((key) => keys.size < count && keys.add(key.substring(key.indexOf(':') + 1)));
|
|
106
|
+
} while (keys.size < count && nextCursor && nextCursor != "0");
|
|
107
|
+
this.debug && console.log(`RedisStore<${this.key}>.scan`, { keys });
|
|
108
|
+
return keys;
|
|
109
|
+
}
|
|
110
|
+
async ids(query = {}) {
|
|
111
|
+
this.debug && console.log(`RedisStore<${this.key}>.ids`, { query });
|
|
112
|
+
if (query.scan) {
|
|
113
|
+
return this.scan(query);
|
|
114
|
+
}
|
|
115
|
+
const min = query.offset || 0;
|
|
116
|
+
const max = min + (query.count || 0) - 1;
|
|
117
|
+
delete query.offset;
|
|
118
|
+
delete query.count;
|
|
119
|
+
const queryEntries = query && Object.entries(query);
|
|
120
|
+
// TODO: support more than one
|
|
121
|
+
if ((queryEntries === null || queryEntries === void 0 ? void 0 : queryEntries.length) > 1) {
|
|
122
|
+
throw `redis.find(query) only supports a single query entry pair`;
|
|
123
|
+
}
|
|
124
|
+
let ids = [];
|
|
125
|
+
const queryEntry = queryEntries && queryEntries[0];
|
|
126
|
+
const [queryKey, queryVal] = queryEntry || [];
|
|
127
|
+
if (queryKey == "id" && Array.isArray(queryVal)) {
|
|
128
|
+
this.debug && console.log(`RedisStore<${this.key}>.ids special case: query is for IDs`, { ids: queryVal });
|
|
129
|
+
ids = queryVal;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
if (queryKey) {
|
|
133
|
+
/* NOT SUPPORTED FOR NOW
|
|
134
|
+
if (queryVal == "*") {
|
|
135
|
+
// lookup keys via the foos:bars lookup set
|
|
136
|
+
keys = (await this.kv.zrange(`${this.setKey}:${queryKey}s`, 0, -1))
|
|
137
|
+
// @ts-ignore
|
|
138
|
+
.map((key: string) => `${this.key}:${key}`);
|
|
139
|
+
} else */ if (queryVal) {
|
|
140
|
+
// lookup keys via the foos:bar:123 lookup set
|
|
141
|
+
// @ts-ignore
|
|
142
|
+
ids = await this.redis.zrange(`${this.setKey}:${queryKey}:${queryVal}`, min, max, { rev: true });
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
throw `redis.find(query) query must have key and value`;
|
|
146
|
+
}
|
|
147
|
+
this.debug && console.log(`RedisStore<${this.key}>.ids queried lookup key`, { query, ids });
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
// get all keys via the index set
|
|
151
|
+
// @ts-ignore
|
|
152
|
+
ids = await this.redis.zrange(`${this.setKey}`, min, max, { rev: true });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return new Set(ids);
|
|
156
|
+
}
|
|
157
|
+
async find(query = {}) {
|
|
158
|
+
this.debug && console.log(`RedisStore<${this.key}>.find`, { query });
|
|
159
|
+
const keys = Array.isArray(query.id)
|
|
160
|
+
? Array.from(await this.ids(query))
|
|
161
|
+
.map((id) => id && this.valueKey(id))
|
|
162
|
+
.filter(Boolean)
|
|
163
|
+
: Array.from(await this.ids(query))
|
|
164
|
+
// @ts-ignore
|
|
165
|
+
.map((key) => `${this.key}:${key}`);
|
|
166
|
+
if (keys.length > 100) {
|
|
167
|
+
console.warn(`RedisStore.RedisStore<${this.key}>.find WARNING: json.mget more than 100 values`, { keys });
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
this.debug && console.log(`RedisStore<${this.key}>.find`, { keys });
|
|
171
|
+
}
|
|
172
|
+
// don't mget too many at once otherwise 💥
|
|
173
|
+
const blockSize = 512;
|
|
174
|
+
const blocks = keys && keys.length && Array
|
|
175
|
+
.apply(null, Array(Math.ceil(keys.length / blockSize)))
|
|
176
|
+
.map((v, block) => (keys || [])
|
|
177
|
+
.slice(blockSize * block, blockSize * (block + 1)));
|
|
178
|
+
this.debug && console.log(`RedisStore<${this.key}>.find`, { blocks });
|
|
179
|
+
const values = blocks && blocks.length > 0
|
|
180
|
+
? (await Promise.all(blocks
|
|
181
|
+
.map(async (keys) => (await this.redis.json.mget(keys, "$"))
|
|
182
|
+
.flat())))
|
|
183
|
+
.flat()
|
|
184
|
+
.filter((value) => value && !value.deletedAt)
|
|
185
|
+
: [];
|
|
186
|
+
this.debug && console.log(`RedisStore<${this.key}>.find`, { values });
|
|
187
|
+
return values;
|
|
188
|
+
}
|
|
189
|
+
async create(value, options) {
|
|
190
|
+
this.debug && console.log(`RedisStore<${this.key}>.create`, { value, options, recordOptions: this.recordOptions });
|
|
191
|
+
const now = (0, moment_1.default)().valueOf();
|
|
192
|
+
options = { ...this.recordOptions, ...options };
|
|
193
|
+
const createdValue = {
|
|
194
|
+
id: value.id || (0, utils_1.uuid)(),
|
|
195
|
+
createdAt: value.createdAt || now,
|
|
196
|
+
...value,
|
|
197
|
+
};
|
|
198
|
+
this.debug && console.log(`RedisStore<${this.key}>.create`, { createdValue });
|
|
199
|
+
const lookupKeys = this.lookupKeys(createdValue, options);
|
|
200
|
+
this.debug && console.log(`RedisStore<${this.key}>.create`, { lookupKeys });
|
|
201
|
+
const responses = await Promise.all([
|
|
202
|
+
this.redis.json.set(this.valueKey(createdValue.id), "$", createdValue),
|
|
203
|
+
options.expire && this.redis.expire(this.valueKey(createdValue.id), options.expire),
|
|
204
|
+
!options.noIndex && this.redis.zadd(this.setKey, { score: createdValue.createdAt, member: createdValue.id }),
|
|
205
|
+
...(lookupKeys ? lookupKeys.map((lookupKey) => this.redis.zadd(lookupKey[0], { score: createdValue.createdAt, member: lookupKey[1] })) : []),
|
|
206
|
+
]);
|
|
207
|
+
this.debug && console.log(`RedisStore<${this.key}>.create`, { responses });
|
|
208
|
+
return createdValue;
|
|
209
|
+
}
|
|
210
|
+
async update(value, options) {
|
|
211
|
+
this.debug && console.log(`RedisStore<${this.key}>.update`, { value, options });
|
|
212
|
+
if (!value.id) {
|
|
213
|
+
throw `Cannot update ${this.key}: null id`;
|
|
214
|
+
}
|
|
215
|
+
const prevValue = await this.get(value.id);
|
|
216
|
+
if (!prevValue) {
|
|
217
|
+
throw `Cannot update ${this.key}: does not exist: ${value.id}`;
|
|
218
|
+
}
|
|
219
|
+
const now = (0, moment_1.default)().valueOf();
|
|
220
|
+
options = { ...this.recordOptions, ...options };
|
|
221
|
+
const updatedValue = {
|
|
222
|
+
...value,
|
|
223
|
+
updatedAt: now,
|
|
224
|
+
};
|
|
225
|
+
// optionally update lookups
|
|
226
|
+
// @ts-ignore
|
|
227
|
+
const prevLookupKeys = new Map(this.lookupKeys(prevValue, options));
|
|
228
|
+
// @ts-ignore
|
|
229
|
+
const lookupKeys = new Map(this.lookupKeys(updatedValue, options));
|
|
230
|
+
const lookupsToRemove = prevLookupKeys && Array.from(prevLookupKeys)
|
|
231
|
+
.filter(([k, v]) => !lookupKeys || lookupKeys.get(k) != v);
|
|
232
|
+
const lookupsToAdd = lookupKeys && Array.from(lookupKeys)
|
|
233
|
+
.filter(([k, v]) => !prevLookupKeys || prevLookupKeys.get(k) != v);
|
|
234
|
+
this.debug && console.log(`RedisStore<${this.key}>.update`, { prevLookupKeys, lookupKeys, prevLookupKeyMap: prevLookupKeys, keysToRemove: lookupsToRemove, keysToAdd: lookupsToAdd });
|
|
235
|
+
if (lookupsToRemove && lookupsToRemove.length) {
|
|
236
|
+
this.debug && console.log(`RedisStore<${this.key}>.update deleting previous lookup keys`, { prevLookupKeys });
|
|
237
|
+
const response = await Promise.all([
|
|
238
|
+
...lookupsToRemove.map((lookupKey) => this.redis.zrem(lookupKey[0], lookupKey[1]))
|
|
239
|
+
]);
|
|
240
|
+
this.debug && console.log(`RedisStore<${this.key}>.update deleted previous lookup keys`, { response });
|
|
241
|
+
}
|
|
242
|
+
const response = await Promise.all([
|
|
243
|
+
this.redis.json.set(this.valueKey(value.id), "$", updatedValue),
|
|
244
|
+
options.expire && this.redis.expire(this.valueKey(value.id), options.expire),
|
|
245
|
+
...(lookupsToAdd ? lookupsToAdd.map((lookupKey) => this.redis.zadd(lookupKey[0], { score: updatedValue.createdAt || updatedValue.updatedAt, member: lookupKey[1] })) : []),
|
|
246
|
+
]);
|
|
247
|
+
this.debug && console.log(`RedisStore<${this.key}>.update`, { response });
|
|
248
|
+
return updatedValue;
|
|
249
|
+
}
|
|
250
|
+
async delete(id, options = {}) {
|
|
251
|
+
this.debug && console.log(`RedisStore<${this.key}>.delete`, { id, options });
|
|
252
|
+
if (!id) {
|
|
253
|
+
throw `Cannot delete ${this.key}: null id`;
|
|
254
|
+
}
|
|
255
|
+
options = { ...this.recordOptions, ...options };
|
|
256
|
+
const value = await this.get(id);
|
|
257
|
+
if (!value) {
|
|
258
|
+
throw `Cannot update ${this.key}: does not exist: ${id}`;
|
|
259
|
+
}
|
|
260
|
+
const lookupKeys = this.lookupKeys(value, options);
|
|
261
|
+
this.debug && console.log(`RedisStore<${this.key}>.delete`, { lookupKeys });
|
|
262
|
+
value.deletedAt = (0, moment_1.default)().valueOf();
|
|
263
|
+
const response = await Promise.all([
|
|
264
|
+
options.hardDelete
|
|
265
|
+
? this.redis.json.del(this.valueKey(id), "$")
|
|
266
|
+
: this.redis.json.set(this.valueKey(id), "$", { ...value, deletedAt: (0, moment_1.default)().valueOf() }),
|
|
267
|
+
this.redis.zrem(this.setKey, id),
|
|
268
|
+
...(lookupKeys ? lookupKeys.map((lookupKey) => this.redis.zrem(lookupKey[0], lookupKey[1])) : []),
|
|
269
|
+
]);
|
|
270
|
+
this.debug && console.log(`RedisStore<${this.key}>.delete`, { response });
|
|
271
|
+
return value;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
exports.RedisStore = RedisStore;
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@desmat/redis-store",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/desmat/redis-store.git"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"utility",
|
|
23
|
+
"library",
|
|
24
|
+
"redis",
|
|
25
|
+
"store"
|
|
26
|
+
],
|
|
27
|
+
"author": "Mathieu Desjarlais",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@desmat/utils": "^1.0.0",
|
|
31
|
+
"@upstash/redis": "^1.34.3"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"typescript": "^4.0.0"
|
|
35
|
+
}
|
|
36
|
+
}
|