@desmat/redis-store 1.2.0 → 1.3.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/dist/index copy.d.ts +60 -0
- package/dist/index copy.js +356 -0
- package/dist/index.d.ts +28 -3
- package/dist/index.js +55 -6
- package/dist/memory-store.d.ts +54 -0
- package/dist/memory-store.js +279 -0
- package/dist/store.d.ts +37 -0
- package/dist/store.js +2 -0
- package/package.json +3 -2
|
@@ -0,0 +1,60 @@
|
|
|
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 default class RedisStore<T extends RedisStoreRecord> {
|
|
9
|
+
redis: Redis;
|
|
10
|
+
key: string;
|
|
11
|
+
setKey: string;
|
|
12
|
+
valueKey: (id: string) => string;
|
|
13
|
+
options: any;
|
|
14
|
+
debug: boolean;
|
|
15
|
+
constructor({ url, token, key, setKey, options, debug, }: {
|
|
16
|
+
key: string;
|
|
17
|
+
setKey?: string;
|
|
18
|
+
options?: any;
|
|
19
|
+
url?: string;
|
|
20
|
+
token?: string;
|
|
21
|
+
debug?: boolean;
|
|
22
|
+
});
|
|
23
|
+
lookupKeys(value: any, options?: {
|
|
24
|
+
noLookup?: boolean;
|
|
25
|
+
lookups?: any;
|
|
26
|
+
}): any[][];
|
|
27
|
+
exists(id: string): Promise<boolean>;
|
|
28
|
+
get(id: string, options?: any): Promise<T | undefined>;
|
|
29
|
+
scan(query?: any): Promise<Set<string>>;
|
|
30
|
+
ids(query?: any, options?: {
|
|
31
|
+
withScores?: boolean;
|
|
32
|
+
aggregate?: string;
|
|
33
|
+
}): Promise<Set<string>>;
|
|
34
|
+
find(query?: any): Promise<T[]>;
|
|
35
|
+
create(value: any, options?: {
|
|
36
|
+
expire?: number;
|
|
37
|
+
noIndex?: boolean;
|
|
38
|
+
score?: number;
|
|
39
|
+
noLookup?: boolean;
|
|
40
|
+
lookups?: any;
|
|
41
|
+
}): Promise<T>;
|
|
42
|
+
update(value: any, options?: any): Promise<T>;
|
|
43
|
+
incrementCounters(values: Record<string, string | number>, delta: {
|
|
44
|
+
total: number;
|
|
45
|
+
count: number;
|
|
46
|
+
}): Promise<any>;
|
|
47
|
+
queryCounter(kind: "count" | "counts" | "totals", dims: string[], exact: Record<string, string | number>, range?: {
|
|
48
|
+
field: string;
|
|
49
|
+
min?: string;
|
|
50
|
+
max?: string;
|
|
51
|
+
}): Promise<number | Array<{
|
|
52
|
+
member: string;
|
|
53
|
+
score: number;
|
|
54
|
+
}>>;
|
|
55
|
+
delete(id: string, options?: {
|
|
56
|
+
hardDelete?: boolean;
|
|
57
|
+
noLookup?: boolean;
|
|
58
|
+
lookups?: any;
|
|
59
|
+
}): Promise<T | undefined>;
|
|
60
|
+
}
|
|
@@ -0,0 +1,356 @@
|
|
|
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
|
+
const moment_1 = __importDefault(require("moment"));
|
|
23
|
+
const utils_1 = require("@desmat/utils");
|
|
24
|
+
const redis_1 = require("@upstash/redis");
|
|
25
|
+
// polyfill Set.intersection
|
|
26
|
+
;
|
|
27
|
+
(function () {
|
|
28
|
+
// @ts-ignore
|
|
29
|
+
if (!Set.prototype.intersection) {
|
|
30
|
+
// @ts-ignore
|
|
31
|
+
Set.prototype.intersection = function (other) {
|
|
32
|
+
return new Set(Array.from(this).filter((value) => other.has(value)));
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
})();
|
|
36
|
+
class RedisStore {
|
|
37
|
+
constructor({ url, token, key, setKey, options, debug, }) {
|
|
38
|
+
const _url = url || process.env.KV_REST_API_URL; //|| "https://crisp-skylark-47483.upstash.io";
|
|
39
|
+
const _token = token || process.env.KV_REST_API_TOKEN; //|| "Abl7AAIncDFiZDM3ZjVjOTEyMmI0MWYxOWRkYTlhMWVkYWQ3ZGRjN3AxNDc0ODM";
|
|
40
|
+
if (!_url)
|
|
41
|
+
throw 'Error creating RedisStore: `url` is required: either provide in constructor or via environment variable `KV_REST_API_URL`';
|
|
42
|
+
if (!_token)
|
|
43
|
+
throw 'Error creating RedisStore: `token` is required: either provide in constructor or via environment variable `KV_REST_API_TOKEN`';
|
|
44
|
+
this.redis = new redis_1.Redis({
|
|
45
|
+
url: _url,
|
|
46
|
+
token: _token
|
|
47
|
+
});
|
|
48
|
+
this.key = key;
|
|
49
|
+
this.setKey = setKey || key + "s";
|
|
50
|
+
this.valueKey = (id) => `${key}:${id}`;
|
|
51
|
+
this.options = options;
|
|
52
|
+
this.debug = !!debug;
|
|
53
|
+
}
|
|
54
|
+
lookupKeys(value, options) {
|
|
55
|
+
options = { ...this.options, ...options };
|
|
56
|
+
this.debug && console.log(`RedisStore.lookupKeys<${this.key}>.lookupKeys`, { value, options });
|
|
57
|
+
/*
|
|
58
|
+
create index and lookup sets based on options.lookups
|
|
59
|
+
|
|
60
|
+
given a likedhaiku record:
|
|
61
|
+
{
|
|
62
|
+
id: 123:456,
|
|
63
|
+
userId: 123,
|
|
64
|
+
haikuId 456,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
and lookups:
|
|
68
|
+
{
|
|
69
|
+
user: { userId: "haikuId"},
|
|
70
|
+
haiku: { haikuId: "userId" }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
we want indexes:
|
|
74
|
+
|
|
75
|
+
likedhaiku:123:456 -> value (JSON, the rest are sorted sets, not handled here)
|
|
76
|
+
likedhaikus -> all likedhaiku id's (ie 123:456, etc, not handled here)
|
|
77
|
+
// 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
|
|
78
|
+
likedhaikus:user:123 -> all likedhaiku id's for the given user (ie 123:456, etc)
|
|
79
|
+
// 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
|
|
80
|
+
likedhaikus:haiku:456 -> all likedhaiku id's for the given haiku (ie 123:456, etc)
|
|
81
|
+
|
|
82
|
+
*/
|
|
83
|
+
const lookupKeys = !(options === null || options === void 0 ? void 0 : options.noLookup) && Object
|
|
84
|
+
.entries((options === null || options === void 0 ? void 0 : options.lookups) || {})
|
|
85
|
+
.map((entry) => {
|
|
86
|
+
const id = value.id;
|
|
87
|
+
const lookupName = entry[0];
|
|
88
|
+
const lookupKey = entry[1];
|
|
89
|
+
// TODO validate and log errors
|
|
90
|
+
// @ts-ignore
|
|
91
|
+
const lookupId = value[lookupKey];
|
|
92
|
+
// foos:bar:123 -> 123:456
|
|
93
|
+
return [`${this.setKey}:${lookupName}:${lookupId}`, id];
|
|
94
|
+
}) || [];
|
|
95
|
+
this.debug && console.log(`RedisStore<${this.key}>.lookupKeys`, { options, lookupKeys });
|
|
96
|
+
return lookupKeys;
|
|
97
|
+
}
|
|
98
|
+
async exists(id) {
|
|
99
|
+
this.debug && console.log(`RedisStore<${this.key}>.exists`, { id });
|
|
100
|
+
const response = await this.redis.exists(this.valueKey(id));
|
|
101
|
+
this.debug && console.log(`RedisStore<${this.key}>.exists`, { response });
|
|
102
|
+
return response > 0;
|
|
103
|
+
}
|
|
104
|
+
async get(id, options) {
|
|
105
|
+
this.debug && console.log(`RedisStore<${this.key}>.get`, { id });
|
|
106
|
+
const response = await this.redis.json.get(this.valueKey(id), "$");
|
|
107
|
+
this.debug && console.log(`RedisStore<${this.key}>.get`, { response });
|
|
108
|
+
let value;
|
|
109
|
+
if (response && response[0] && !(response[0].deletedAt && !(options === null || options === void 0 ? void 0 : options.deleted))) {
|
|
110
|
+
value = response[0];
|
|
111
|
+
}
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
async scan(query = {}) {
|
|
115
|
+
this.debug && console.log(`RedisStore<${this.key}>.scan`, { query });
|
|
116
|
+
!query.count && console.warn(`RedisStore.RedisStore<${this.key}>.find WARNING: scan command with no count provided: setting count at 999`);
|
|
117
|
+
const count = query.count || 999;
|
|
118
|
+
const match = this.valueKey(query.scan);
|
|
119
|
+
let keys = new Set();
|
|
120
|
+
let nextCursor = "0";
|
|
121
|
+
do {
|
|
122
|
+
const ret = await this.redis.scan(nextCursor, { match, type: "json", count: count - keys.size });
|
|
123
|
+
this.debug && console.log(`RedisStore<${this.key}>.scan`, { ret });
|
|
124
|
+
nextCursor = ret[0];
|
|
125
|
+
ret[1].forEach((key) => keys.size < count && keys.add(key.substring(key.indexOf(':') + 1)));
|
|
126
|
+
} while (keys.size < count && nextCursor && nextCursor != "0");
|
|
127
|
+
this.debug && console.log(`RedisStore<${this.key}>.scan`, { keys });
|
|
128
|
+
return keys;
|
|
129
|
+
}
|
|
130
|
+
async ids(query = {}, options) {
|
|
131
|
+
this.debug && console.log(`RedisStore<${this.key}>.ids`, { query });
|
|
132
|
+
if (query.scan) {
|
|
133
|
+
return this.scan(query);
|
|
134
|
+
}
|
|
135
|
+
let count = query.count;
|
|
136
|
+
delete query.count;
|
|
137
|
+
if (typeof (count) != "undefined" && typeof (count) != "number") {
|
|
138
|
+
console.warn(`RedisStore.RedisStore<${this.key}>.id WARNING: query.count is not a number`);
|
|
139
|
+
count = undefined;
|
|
140
|
+
}
|
|
141
|
+
let offset = query.offset;
|
|
142
|
+
delete query.offset;
|
|
143
|
+
if (typeof (offset) != "undefined" && typeof (offset) != "number") {
|
|
144
|
+
console.warn(`RedisStore.RedisStore<${this.key}>.id WARNING: query.offset must be a number`);
|
|
145
|
+
offset = undefined;
|
|
146
|
+
}
|
|
147
|
+
const min = offset || 0;
|
|
148
|
+
const max = min + (count || 0) - 1;
|
|
149
|
+
const queryEntries = query && Object.entries(query);
|
|
150
|
+
if (options === null || options === void 0 ? void 0 : options.withScores) {
|
|
151
|
+
// const setOfIds = await Promise.all(
|
|
152
|
+
// queryEntries.map(([queryKey, queryVal]: [string, string]) => {
|
|
153
|
+
// if (queryVal) {
|
|
154
|
+
// // lookup keys via the foos:bar:123 lookup set
|
|
155
|
+
// return this.redis.zrange(`${this.setKey}:${queryKey}:${queryVal}`, min, max, { rev: true, withScores: !!options?.withScores });
|
|
156
|
+
// } else {
|
|
157
|
+
// throw `redis.find(query) query must have key and value`;
|
|
158
|
+
// }
|
|
159
|
+
// })
|
|
160
|
+
// );
|
|
161
|
+
const keys = queryEntries.map(([queryKey, queryVal]) => `${queryKey}:${queryVal}`);
|
|
162
|
+
const overallKey = `${this.setKey}:${keys.join(":")}`;
|
|
163
|
+
const zinterstoreRet = await this.redis.zinterstore(overallKey, keys.length, keys.map((k) => `${this.setKey}:${k}`), { aggregate: "min" });
|
|
164
|
+
const zrangeRet = await this.redis.zrange(overallKey, min, max, { rev: true, withScores: !!(options === null || options === void 0 ? void 0 : options.withScores) });
|
|
165
|
+
console.warn(`RedisStore.RedisStore<${this.key}>.ids`, { overallKey, keys, zinterstoreRet, zrangeRet });
|
|
166
|
+
// const ids = setOfIds.reduce((prev: Set<any> | undefined, curr: any[]) => {
|
|
167
|
+
// new Set(curr).intersection(prev || new Set(curr)), undefined)
|
|
168
|
+
// throw 'NOT IMPLEMENTED';
|
|
169
|
+
}
|
|
170
|
+
// .ids() or .ids({})
|
|
171
|
+
if (!(queryEntries === null || queryEntries === void 0 ? void 0 : queryEntries.length)) {
|
|
172
|
+
// get all keys via the index set
|
|
173
|
+
return new Set(await this.redis.zrange(`${this.setKey}`, min, max, { rev: true /* , withScores: !!options?.withScores */ }));
|
|
174
|
+
}
|
|
175
|
+
// .ids({ foo: "FOO", bar: "BAR", ... })
|
|
176
|
+
const setOfIds = await Promise.all(queryEntries.map(([queryKey, queryVal]) => {
|
|
177
|
+
if (queryVal) {
|
|
178
|
+
// lookup keys via the foos:bar:123 lookup set
|
|
179
|
+
return this.redis.zrange(`${this.setKey}:${queryKey}:${queryVal}`, min, max, { rev: true /* , withScores: !!options?.withScores */ });
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
throw `redis.find(query) query must have key and value`;
|
|
183
|
+
}
|
|
184
|
+
}));
|
|
185
|
+
// @ts-ignore
|
|
186
|
+
const ids = setOfIds.reduce((prev, curr) => new Set(curr).intersection(prev || new Set(curr)), undefined);
|
|
187
|
+
this.debug && console.log(`RedisStore<${this.key}>.ids queried lookup key`, { query, setOfIds, ids });
|
|
188
|
+
return ids;
|
|
189
|
+
}
|
|
190
|
+
async find(query = {}) {
|
|
191
|
+
this.debug && console.log(`RedisStore<${this.key}>.find`, { query });
|
|
192
|
+
const keys = Array.isArray(query.id)
|
|
193
|
+
? query.id
|
|
194
|
+
.map((id) => id && this.valueKey(id))
|
|
195
|
+
.filter(Boolean)
|
|
196
|
+
: Array.from(await this.ids(query))
|
|
197
|
+
// @ts-ignore
|
|
198
|
+
.map((key) => `${this.key}:${key}`);
|
|
199
|
+
if (keys.length > 100) {
|
|
200
|
+
console.warn(`RedisStore.RedisStore<${this.key}>.find WARNING: json.mget more than 100 values`, { keys });
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
this.debug && console.log(`RedisStore<${this.key}>.find`, { keys });
|
|
204
|
+
}
|
|
205
|
+
// don't mget too many at once otherwise 💥
|
|
206
|
+
const blockSize = 256;
|
|
207
|
+
const blocks = keys && keys.length && Array
|
|
208
|
+
.apply(null, Array(Math.ceil(keys.length / blockSize)))
|
|
209
|
+
.map((v, block) => (keys || [])
|
|
210
|
+
.slice(blockSize * block, blockSize * (block + 1)));
|
|
211
|
+
this.debug && console.log(`RedisStore<${this.key}>.find`, { blocks });
|
|
212
|
+
const values = blocks && blocks.length > 0
|
|
213
|
+
? (await Promise.all(blocks
|
|
214
|
+
.map(async (keys) => (await this.redis.json.mget(keys, "$"))
|
|
215
|
+
.flat())))
|
|
216
|
+
.flat()
|
|
217
|
+
.filter((value) => value && !value.deletedAt)
|
|
218
|
+
: [];
|
|
219
|
+
this.debug && console.log(`RedisStore<${this.key}>.find`, { values });
|
|
220
|
+
return values;
|
|
221
|
+
}
|
|
222
|
+
async create(value, options) {
|
|
223
|
+
this.debug && console.log(`RedisStore<${this.key}>.create`, { value, options, this_options: this.options });
|
|
224
|
+
const now = (0, moment_1.default)().valueOf();
|
|
225
|
+
options = { ...this.options, ...options };
|
|
226
|
+
const createdValue = {
|
|
227
|
+
id: value.id || (0, utils_1.uuid)(),
|
|
228
|
+
createdAt: value.createdAt || now,
|
|
229
|
+
...value,
|
|
230
|
+
};
|
|
231
|
+
this.debug && console.log(`RedisStore<${this.key}>.create`, { createdValue });
|
|
232
|
+
const lookupKeys = this.lookupKeys(createdValue, options);
|
|
233
|
+
this.debug && console.log(`RedisStore<${this.key}>.create`, { lookupKeys });
|
|
234
|
+
const responses = await Promise.all([
|
|
235
|
+
this.redis.json.set(this.valueKey(createdValue.id), "$", createdValue),
|
|
236
|
+
(options === null || options === void 0 ? void 0 : options.expire) && this.redis.expire(this.valueKey(createdValue.id), options === null || options === void 0 ? void 0 : options.expire),
|
|
237
|
+
// !options?.noIndex && this.redis.zadd(this.setKey, { score: options?.score || createdValue.createdAt, member: createdValue.id }),
|
|
238
|
+
// ...(lookupKeys ? lookupKeys.map((lookupKey: any) => this.redis.zadd(lookupKey[0], { score: options?.score || createdValue.createdAt, member: lookupKey[1] })) : []),
|
|
239
|
+
...(lookupKeys ? lookupKeys.map((lookupKey) => this.redis.zincrby(lookupKey[0], (options === null || options === void 0 ? void 0 : options.score) || createdValue.createdAt, lookupKey[1])) : []),
|
|
240
|
+
]);
|
|
241
|
+
this.debug && console.log(`RedisStore<${this.key}>.create`, { responses });
|
|
242
|
+
return createdValue;
|
|
243
|
+
}
|
|
244
|
+
async update(value, options) {
|
|
245
|
+
this.debug && console.log(`RedisStore<${this.key}>.update`, { value, options });
|
|
246
|
+
if (!value.id) {
|
|
247
|
+
throw `Cannot update ${this.key}: null id`;
|
|
248
|
+
}
|
|
249
|
+
const prevValue = await this.get(value.id);
|
|
250
|
+
if (!prevValue) {
|
|
251
|
+
throw `Cannot update ${this.key}: does not exist: ${value.id}`;
|
|
252
|
+
}
|
|
253
|
+
const now = (0, moment_1.default)().valueOf();
|
|
254
|
+
options = { ...this.options, ...options };
|
|
255
|
+
const updatedValue = {
|
|
256
|
+
...value,
|
|
257
|
+
updatedAt: now,
|
|
258
|
+
};
|
|
259
|
+
// optionally update lookups
|
|
260
|
+
// @ts-ignore
|
|
261
|
+
const prevLookupKeys = new Map(this.lookupKeys(prevValue, options));
|
|
262
|
+
// @ts-ignore
|
|
263
|
+
const lookupKeys = new Map(this.lookupKeys(updatedValue, options));
|
|
264
|
+
const lookupsToRemove = prevLookupKeys && Array.from(prevLookupKeys)
|
|
265
|
+
.filter(([k, v]) => !lookupKeys || lookupKeys.get(k) != v);
|
|
266
|
+
const lookupsToAdd = lookupKeys && Array.from(lookupKeys)
|
|
267
|
+
.filter(([k, v]) => !prevLookupKeys || prevLookupKeys.get(k) != v);
|
|
268
|
+
this.debug && console.log(`RedisStore<${this.key}>.update`, { prevLookupKeys, lookupKeys, prevLookupKeyMap: prevLookupKeys, keysToRemove: lookupsToRemove, keysToAdd: lookupsToAdd });
|
|
269
|
+
if (lookupsToRemove && lookupsToRemove.length) {
|
|
270
|
+
this.debug && console.log(`RedisStore<${this.key}>.update deleting previous lookup keys`, { prevLookupKeys });
|
|
271
|
+
const response = await Promise.all([
|
|
272
|
+
...lookupsToRemove.map((lookupKey) => this.redis.zrem(lookupKey[0], lookupKey[1]))
|
|
273
|
+
]);
|
|
274
|
+
this.debug && console.log(`RedisStore<${this.key}>.update deleted previous lookup keys`, { response });
|
|
275
|
+
}
|
|
276
|
+
const response = await Promise.all([
|
|
277
|
+
this.redis.json.set(this.valueKey(value.id), "$", updatedValue),
|
|
278
|
+
options.expire && this.redis.expire(this.valueKey(value.id), options.expire),
|
|
279
|
+
...(lookupsToAdd ? lookupsToAdd.map((lookupKey) => this.redis.zadd(lookupKey[0], { score: updatedValue.createdAt || updatedValue.updatedAt, member: lookupKey[1] })) : []),
|
|
280
|
+
]);
|
|
281
|
+
this.debug && console.log(`RedisStore<${this.key}>.update`, { response });
|
|
282
|
+
return updatedValue;
|
|
283
|
+
}
|
|
284
|
+
async incrementCounters(values, delta) {
|
|
285
|
+
var _a;
|
|
286
|
+
this.debug && console.log(`RedisStore<${this.key}>.incrementCounters`, { values, delta });
|
|
287
|
+
const counters = ((_a = this.options) === null || _a === void 0 ? void 0 : _a.counters) || [];
|
|
288
|
+
const responses = await Promise.all(counters
|
|
289
|
+
.filter((dims) => dims.every((d) => typeof values[d] != "undefined"))
|
|
290
|
+
.flatMap((dims) => {
|
|
291
|
+
const member = dims.map((d) => `${d}=${values[d]}`).join(":");
|
|
292
|
+
return [
|
|
293
|
+
this.redis.zincrby(`${this.key}Totals:${dims.join(":")}`, delta.total, member),
|
|
294
|
+
this.redis.zincrby(`${this.key}Counts:${dims.join(":")}`, delta.count, member),
|
|
295
|
+
];
|
|
296
|
+
}));
|
|
297
|
+
this.debug && console.log(`RedisStore<${this.key}>.incrementCounters`, { responses });
|
|
298
|
+
return responses;
|
|
299
|
+
}
|
|
300
|
+
async queryCounter(kind, dims, exact, range) {
|
|
301
|
+
this.debug && console.log(`RedisStore<${this.key}>.queryCounter`, { kind, dims, exact, range });
|
|
302
|
+
const setKey = `${this.key}${kind == "totals" ? "Totals" : "Counts"}:${dims.join(":")}`;
|
|
303
|
+
const prefix = dims
|
|
304
|
+
.filter((d) => typeof exact[d] != "undefined")
|
|
305
|
+
.map((d) => `${d}=${exact[d]}`)
|
|
306
|
+
.join(":");
|
|
307
|
+
// the trailing sentinel scopes the range to the "prefix:" family regardless of whether
|
|
308
|
+
// the range field itself is the last dim, matching how the original hand-rolled queries worked
|
|
309
|
+
const hasMore = dims.length > Object.keys(exact).length;
|
|
310
|
+
const minPrefix = (range === null || range === void 0 ? void 0 : range.min) != null ? `${prefix}${prefix ? ":" : ""}${range.field}=${range.min}` : prefix;
|
|
311
|
+
const maxPrefix = (range === null || range === void 0 ? void 0 : range.max) != null ? `${prefix}${prefix ? ":" : ""}${range.field}=${range.max}` : prefix;
|
|
312
|
+
const min = `[${minPrefix}${hasMore ? ":\x00" : ""}`;
|
|
313
|
+
const max = `[${maxPrefix}${hasMore ? ":\x7f" : ""}`;
|
|
314
|
+
this.debug && console.log(`RedisStore<${this.key}>.queryCounter`, { setKey, min, max });
|
|
315
|
+
if (kind == "count") {
|
|
316
|
+
const count = await this.redis.zlexcount(setKey, min, max);
|
|
317
|
+
this.debug && console.log(`RedisStore<${this.key}>.queryCounter`, { count });
|
|
318
|
+
return count;
|
|
319
|
+
}
|
|
320
|
+
const raw = await this.redis.zrange(setKey, min, max, { byLex: true, withScores: true });
|
|
321
|
+
this.debug && console.log(`RedisStore<${this.key}>.queryCounter`, { raw });
|
|
322
|
+
// members are returned as-is ("field=value:field=value...") rather than parsed into an
|
|
323
|
+
// object: some field values (eg namespaced ids) may themselves contain the ":" delimiter,
|
|
324
|
+
// which only the caller can safely account for when splitting a member back into fields
|
|
325
|
+
const results = [];
|
|
326
|
+
for (let i = 0; i < raw.length / 2; i++) {
|
|
327
|
+
results.push({ member: `${raw[2 * i]}`, score: raw[2 * i + 1] });
|
|
328
|
+
}
|
|
329
|
+
this.debug && console.log(`RedisStore<${this.key}>.queryCounter`, { results });
|
|
330
|
+
return results;
|
|
331
|
+
}
|
|
332
|
+
async delete(id, options) {
|
|
333
|
+
this.debug && console.log(`RedisStore<${this.key}>.delete`, { id, options });
|
|
334
|
+
if (!id) {
|
|
335
|
+
throw `Cannot delete ${this.key}: null id`;
|
|
336
|
+
}
|
|
337
|
+
options = { ...this.options, ...options };
|
|
338
|
+
const value = await this.get(id, { deleted: true });
|
|
339
|
+
if (!value) {
|
|
340
|
+
console.warn(`RedisStore<${this.key}>.delete WARNING: does not exist: ${id}`);
|
|
341
|
+
}
|
|
342
|
+
const lookupKeys = value && this.lookupKeys(value, options);
|
|
343
|
+
this.debug && console.log(`RedisStore<${this.key}>.delete`, { lookupKeys });
|
|
344
|
+
const deletedAt = (0, moment_1.default)().valueOf();
|
|
345
|
+
const response = await Promise.all([
|
|
346
|
+
(options === null || options === void 0 ? void 0 : options.hardDelete)
|
|
347
|
+
? this.redis.json.del(this.valueKey(id), "$")
|
|
348
|
+
: this.redis.json.set(this.valueKey(id), "$.deletedAt", deletedAt),
|
|
349
|
+
this.redis.zrem(this.setKey, id),
|
|
350
|
+
...(lookupKeys ? lookupKeys.map((lookupKey) => this.redis.zrem(lookupKey[0], lookupKey[1])) : []),
|
|
351
|
+
]);
|
|
352
|
+
this.debug && console.log(`RedisStore<${this.key}>.delete`, { response });
|
|
353
|
+
return value ? { ...value, deletedAt } : undefined;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
exports.default = RedisStore;
|
package/dist/index.d.ts
CHANGED
|
@@ -20,13 +20,38 @@ export default class RedisStore<T extends RedisStoreRecord> {
|
|
|
20
20
|
token?: string;
|
|
21
21
|
debug?: boolean;
|
|
22
22
|
});
|
|
23
|
-
lookupKeys(value: any, options?:
|
|
23
|
+
lookupKeys(value: any, options?: {
|
|
24
|
+
noLookup?: boolean;
|
|
25
|
+
lookups?: any;
|
|
26
|
+
}): any[][];
|
|
24
27
|
exists(id: string): Promise<boolean>;
|
|
25
28
|
get(id: string, options?: any): Promise<T | undefined>;
|
|
26
29
|
scan(query?: any): Promise<Set<string>>;
|
|
27
30
|
ids(query?: any): Promise<Set<string>>;
|
|
28
31
|
find(query?: any): Promise<T[]>;
|
|
29
|
-
create(value: any, options?:
|
|
32
|
+
create(value: any, options?: {
|
|
33
|
+
expire?: number;
|
|
34
|
+
noIndex?: boolean;
|
|
35
|
+
score?: number;
|
|
36
|
+
noLookup?: boolean;
|
|
37
|
+
lookups?: any;
|
|
38
|
+
}): Promise<T>;
|
|
30
39
|
update(value: any, options?: any): Promise<T>;
|
|
31
|
-
|
|
40
|
+
incrementCounters(values: Record<string, string | number>, delta: {
|
|
41
|
+
total: number;
|
|
42
|
+
count: number;
|
|
43
|
+
}): Promise<any>;
|
|
44
|
+
queryCounter(kind: "count" | "counts" | "totals", counter: string, exact: Record<string, string | number>, range?: {
|
|
45
|
+
field: string;
|
|
46
|
+
min?: string;
|
|
47
|
+
max?: string;
|
|
48
|
+
}): Promise<number | Array<{
|
|
49
|
+
member: string;
|
|
50
|
+
score: number;
|
|
51
|
+
}>>;
|
|
52
|
+
delete(id: string, options?: {
|
|
53
|
+
hardDelete?: boolean;
|
|
54
|
+
noLookup?: boolean;
|
|
55
|
+
lookups?: any;
|
|
56
|
+
}): Promise<T | undefined>;
|
|
32
57
|
}
|
package/dist/index.js
CHANGED
|
@@ -135,13 +135,13 @@ class RedisStore {
|
|
|
135
135
|
let count = query.count;
|
|
136
136
|
delete query.count;
|
|
137
137
|
if (typeof (count) != "undefined" && typeof (count) != "number") {
|
|
138
|
-
console.warn(`RedisStore.RedisStore<${this.key}>.id WARNING: query.count is not a
|
|
138
|
+
console.warn(`RedisStore.RedisStore<${this.key}>.id WARNING: query.count is not a number`);
|
|
139
139
|
count = undefined;
|
|
140
140
|
}
|
|
141
141
|
let offset = query.offset;
|
|
142
142
|
delete query.offset;
|
|
143
143
|
if (typeof (offset) != "undefined" && typeof (offset) != "number") {
|
|
144
|
-
console.warn(`RedisStore.RedisStore<${this.key}>.id WARNING: query.offset must be a
|
|
144
|
+
console.warn(`RedisStore.RedisStore<${this.key}>.id WARNING: query.offset must be a number`);
|
|
145
145
|
offset = undefined;
|
|
146
146
|
}
|
|
147
147
|
const min = offset || 0;
|
|
@@ -213,8 +213,8 @@ class RedisStore {
|
|
|
213
213
|
this.debug && console.log(`RedisStore<${this.key}>.create`, { lookupKeys });
|
|
214
214
|
const responses = await Promise.all([
|
|
215
215
|
this.redis.json.set(this.valueKey(createdValue.id), "$", createdValue),
|
|
216
|
-
options.expire && this.redis.expire(this.valueKey(createdValue.id), options.expire),
|
|
217
|
-
!options.noIndex && this.redis.zadd(this.setKey, { score: createdValue.createdAt, member: createdValue.id }),
|
|
216
|
+
(options === null || options === void 0 ? void 0 : options.expire) && this.redis.expire(this.valueKey(createdValue.id), options === null || options === void 0 ? void 0 : options.expire),
|
|
217
|
+
!(options === null || options === void 0 ? void 0 : options.noIndex) && this.redis.zadd(this.setKey, { score: createdValue.createdAt, member: createdValue.id }),
|
|
218
218
|
...(lookupKeys ? lookupKeys.map((lookupKey) => this.redis.zadd(lookupKey[0], { score: createdValue.createdAt, member: lookupKey[1] })) : []),
|
|
219
219
|
]);
|
|
220
220
|
this.debug && console.log(`RedisStore<${this.key}>.create`, { responses });
|
|
@@ -260,7 +260,56 @@ class RedisStore {
|
|
|
260
260
|
this.debug && console.log(`RedisStore<${this.key}>.update`, { response });
|
|
261
261
|
return updatedValue;
|
|
262
262
|
}
|
|
263
|
-
async
|
|
263
|
+
async incrementCounters(values, delta) {
|
|
264
|
+
var _a;
|
|
265
|
+
this.debug && console.log(`RedisStore<${this.key}>.incrementCounters`, { values, delta });
|
|
266
|
+
const counters = ((_a = this.options) === null || _a === void 0 ? void 0 : _a.counters) || [];
|
|
267
|
+
const responses = await Promise.all(counters
|
|
268
|
+
.filter((counter) => counter.split(":").every((d) => typeof values[d] != "undefined"))
|
|
269
|
+
.flatMap((counter) => {
|
|
270
|
+
const member = counter.split(":").map((d) => `${d}=${values[d]}`).join(":");
|
|
271
|
+
return [
|
|
272
|
+
this.redis.zincrby(`${this.key}Totals:${counter}`, delta.total, member),
|
|
273
|
+
this.redis.zincrby(`${this.key}Counts:${counter}`, delta.count, member),
|
|
274
|
+
];
|
|
275
|
+
}));
|
|
276
|
+
this.debug && console.log(`RedisStore<${this.key}>.incrementCounters`, { responses });
|
|
277
|
+
return responses;
|
|
278
|
+
}
|
|
279
|
+
async queryCounter(kind, counter, exact, range) {
|
|
280
|
+
this.debug && console.log(`RedisStore<${this.key}>.queryCounter`, { kind, counter, exact, range });
|
|
281
|
+
const dims = counter.split(":");
|
|
282
|
+
const setKey = `${this.key}${kind == "totals" ? "Totals" : "Counts"}:${counter}`;
|
|
283
|
+
const prefix = dims
|
|
284
|
+
.filter((d) => typeof exact[d] != "undefined")
|
|
285
|
+
.map((d) => `${d}=${exact[d]}`)
|
|
286
|
+
.join(":");
|
|
287
|
+
// the trailing sentinel scopes the range to the "prefix:" family regardless of whether
|
|
288
|
+
// the range field itself is the last dim, matching how the original hand-rolled queries worked
|
|
289
|
+
const hasMore = dims.length > Object.keys(exact).length;
|
|
290
|
+
const minPrefix = (range === null || range === void 0 ? void 0 : range.min) != null ? `${prefix}${prefix ? ":" : ""}${range.field}=${range.min}` : prefix;
|
|
291
|
+
const maxPrefix = (range === null || range === void 0 ? void 0 : range.max) != null ? `${prefix}${prefix ? ":" : ""}${range.field}=${range.max}` : prefix;
|
|
292
|
+
const min = `[${minPrefix}${hasMore ? ":\x00" : ""}`;
|
|
293
|
+
const max = `[${maxPrefix}${hasMore ? ":\x7f" : ""}`;
|
|
294
|
+
this.debug && console.log(`RedisStore<${this.key}>.queryCounter`, { setKey, min, max });
|
|
295
|
+
if (kind == "count") {
|
|
296
|
+
const count = await this.redis.zlexcount(setKey, min, max);
|
|
297
|
+
this.debug && console.log(`RedisStore<${this.key}>.queryCounter`, { count });
|
|
298
|
+
return count;
|
|
299
|
+
}
|
|
300
|
+
const raw = await this.redis.zrange(setKey, min, max, { byLex: true, withScores: true });
|
|
301
|
+
this.debug && console.log(`RedisStore<${this.key}>.queryCounter`, { raw });
|
|
302
|
+
// members are returned as-is ("field=value:field=value...") rather than parsed into an
|
|
303
|
+
// object: some field values (eg namespaced ids) may themselves contain the ":" delimiter,
|
|
304
|
+
// which only the caller can safely account for when splitting a member back into fields
|
|
305
|
+
const results = [];
|
|
306
|
+
for (let i = 0; i < raw.length / 2; i++) {
|
|
307
|
+
results.push({ member: `${raw[2 * i]}`, score: raw[2 * i + 1] });
|
|
308
|
+
}
|
|
309
|
+
this.debug && console.log(`RedisStore<${this.key}>.queryCounter`, { results });
|
|
310
|
+
return results;
|
|
311
|
+
}
|
|
312
|
+
async delete(id, options) {
|
|
264
313
|
this.debug && console.log(`RedisStore<${this.key}>.delete`, { id, options });
|
|
265
314
|
if (!id) {
|
|
266
315
|
throw `Cannot delete ${this.key}: null id`;
|
|
@@ -274,7 +323,7 @@ class RedisStore {
|
|
|
274
323
|
this.debug && console.log(`RedisStore<${this.key}>.delete`, { lookupKeys });
|
|
275
324
|
const deletedAt = (0, moment_1.default)().valueOf();
|
|
276
325
|
const response = await Promise.all([
|
|
277
|
-
options.hardDelete
|
|
326
|
+
(options === null || options === void 0 ? void 0 : options.hardDelete)
|
|
278
327
|
? this.redis.json.del(this.valueKey(id), "$")
|
|
279
328
|
: this.redis.json.set(this.valueKey(id), "$.deletedAt", deletedAt),
|
|
280
329
|
this.redis.zrem(this.setKey, id),
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { RedisStoreRecord } from "./index";
|
|
2
|
+
import type { Store } from "./store";
|
|
3
|
+
export default class MemoryStore<T extends RedisStoreRecord> implements Store<T> {
|
|
4
|
+
key: string;
|
|
5
|
+
setKey: string;
|
|
6
|
+
options: any;
|
|
7
|
+
debug: boolean;
|
|
8
|
+
private records;
|
|
9
|
+
private index;
|
|
10
|
+
private lookups;
|
|
11
|
+
private counters;
|
|
12
|
+
constructor({ key, setKey, options, debug, seed, }: {
|
|
13
|
+
key: string;
|
|
14
|
+
setKey?: string;
|
|
15
|
+
options?: any;
|
|
16
|
+
debug?: boolean;
|
|
17
|
+
seed?: T[];
|
|
18
|
+
});
|
|
19
|
+
lookupKeys(value: any, options?: {
|
|
20
|
+
noLookup?: boolean;
|
|
21
|
+
lookups?: any;
|
|
22
|
+
}): any[][];
|
|
23
|
+
private _index;
|
|
24
|
+
exists(id: string): Promise<boolean>;
|
|
25
|
+
get(id: string, options?: any): 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?: {
|
|
30
|
+
expire?: number;
|
|
31
|
+
noIndex?: boolean;
|
|
32
|
+
score?: number;
|
|
33
|
+
noLookup?: boolean;
|
|
34
|
+
lookups?: any;
|
|
35
|
+
}): Promise<T>;
|
|
36
|
+
update(value: any, options?: any): Promise<T>;
|
|
37
|
+
incCounters(values: Record<string, string | number>, delta: {
|
|
38
|
+
total: number;
|
|
39
|
+
count: number;
|
|
40
|
+
}): Promise<any>;
|
|
41
|
+
queryCounter(kind: "count" | "counts" | "totals", counter: string, exact: Record<string, string | number>, range?: {
|
|
42
|
+
field: string;
|
|
43
|
+
min?: string;
|
|
44
|
+
max?: string;
|
|
45
|
+
}): Promise<number | Array<{
|
|
46
|
+
member: string;
|
|
47
|
+
score: number;
|
|
48
|
+
}>>;
|
|
49
|
+
delete(id: string, options?: {
|
|
50
|
+
hardDelete?: boolean;
|
|
51
|
+
noLookup?: boolean;
|
|
52
|
+
lookups?: any;
|
|
53
|
+
}): Promise<T | undefined>;
|
|
54
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const utils_1 = require("@desmat/utils");
|
|
4
|
+
// mimics a Redis sorted set: members ordered by score, ties broken by member
|
|
5
|
+
// string ascending (Redis zset default ordering), with ZRANGE's index semantics
|
|
6
|
+
// (negative indices count from the end, e.g. -1 is the last element).
|
|
7
|
+
class SortedIdList {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.entries = [];
|
|
10
|
+
}
|
|
11
|
+
add(id, score) {
|
|
12
|
+
this.remove(id);
|
|
13
|
+
this.entries.push({ id, score });
|
|
14
|
+
}
|
|
15
|
+
remove(id) {
|
|
16
|
+
this.entries = this.entries.filter((e) => e.id !== id);
|
|
17
|
+
}
|
|
18
|
+
sorted() {
|
|
19
|
+
return [...this.entries].sort((a, b) => a.score - b.score || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
20
|
+
}
|
|
21
|
+
range(min, max, rev) {
|
|
22
|
+
const ordered = rev ? this.sorted().reverse() : this.sorted();
|
|
23
|
+
const len = ordered.length;
|
|
24
|
+
const resolve = (i) => (i < 0 ? len + i : i);
|
|
25
|
+
const start = Math.max(resolve(min), 0);
|
|
26
|
+
const stop = Math.min(resolve(max), len - 1);
|
|
27
|
+
if (len === 0 || start > stop)
|
|
28
|
+
return [];
|
|
29
|
+
return ordered.slice(start, stop + 1).map((e) => e.id);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function globToRegExp(pattern) {
|
|
33
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
34
|
+
return new RegExp(`^${escaped}$`);
|
|
35
|
+
}
|
|
36
|
+
// intersect() polyfills Set.prototype.intersection so this module has no load-order
|
|
37
|
+
// dependency on index.ts's IIFE that installs it globally.
|
|
38
|
+
function intersect(a, b) {
|
|
39
|
+
return new Set(Array.from(a).filter((value) => b.has(value)));
|
|
40
|
+
}
|
|
41
|
+
class MemoryStore {
|
|
42
|
+
constructor({ key, setKey, options, debug, seed, }) {
|
|
43
|
+
this.records = new Map();
|
|
44
|
+
this.index = new SortedIdList();
|
|
45
|
+
this.lookups = new Map();
|
|
46
|
+
this.counters = new Map();
|
|
47
|
+
this.key = key;
|
|
48
|
+
this.setKey = setKey || key + "s";
|
|
49
|
+
this.options = options;
|
|
50
|
+
this.debug = !!debug;
|
|
51
|
+
(seed || []).forEach((value) => this._index(value));
|
|
52
|
+
}
|
|
53
|
+
lookupKeys(value, options) {
|
|
54
|
+
options = { ...this.options, ...options };
|
|
55
|
+
this.debug && console.log(`MemoryStore<${this.key}>.lookupKeys`, { value, options });
|
|
56
|
+
const lookupKeys = !(options === null || options === void 0 ? void 0 : options.noLookup) && Object
|
|
57
|
+
.entries((options === null || options === void 0 ? void 0 : options.lookups) || {})
|
|
58
|
+
.map((entry) => {
|
|
59
|
+
const id = value.id;
|
|
60
|
+
const lookupName = entry[0];
|
|
61
|
+
const lookupKey = entry[1];
|
|
62
|
+
// @ts-ignore
|
|
63
|
+
const lookupId = value[lookupKey];
|
|
64
|
+
return [`${this.setKey}:${lookupName}:${lookupId}`, id];
|
|
65
|
+
}) || [];
|
|
66
|
+
this.debug && console.log(`MemoryStore<${this.key}>.lookupKeys`, { options, lookupKeys });
|
|
67
|
+
return lookupKeys;
|
|
68
|
+
}
|
|
69
|
+
// shared by the constructor's `seed` and create(): adds a record to the main
|
|
70
|
+
// index and lookup indices without re-deriving id/createdAt defaults.
|
|
71
|
+
_index(value, options) {
|
|
72
|
+
this.records.set(value.id, value);
|
|
73
|
+
!(options === null || options === void 0 ? void 0 : options.noIndex) && this.index.add(value.id, value.createdAt);
|
|
74
|
+
const lookupKeys = this.lookupKeys(value, options);
|
|
75
|
+
(lookupKeys || []).forEach(([lookupKey, id]) => {
|
|
76
|
+
if (!this.lookups.has(lookupKey))
|
|
77
|
+
this.lookups.set(lookupKey, new SortedIdList());
|
|
78
|
+
this.lookups.get(lookupKey).add(id, value.createdAt);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async exists(id) {
|
|
82
|
+
this.debug && console.log(`MemoryStore<${this.key}>.exists`, { id });
|
|
83
|
+
return this.records.has(id);
|
|
84
|
+
}
|
|
85
|
+
async get(id, options) {
|
|
86
|
+
this.debug && console.log(`MemoryStore<${this.key}>.get`, { id });
|
|
87
|
+
const value = this.records.get(id);
|
|
88
|
+
return value && !(value.deletedAt && !(options === null || options === void 0 ? void 0 : options.deleted)) ? value : undefined;
|
|
89
|
+
}
|
|
90
|
+
async scan(query = {}) {
|
|
91
|
+
var _a;
|
|
92
|
+
this.debug && console.log(`MemoryStore<${this.key}>.scan`, { query });
|
|
93
|
+
const count = query.count || 999;
|
|
94
|
+
const pattern = globToRegExp((_a = query.scan) !== null && _a !== void 0 ? _a : "*");
|
|
95
|
+
const keys = new Set();
|
|
96
|
+
for (const id of this.records.keys()) {
|
|
97
|
+
if (keys.size >= count)
|
|
98
|
+
break;
|
|
99
|
+
if (pattern.test(id))
|
|
100
|
+
keys.add(id);
|
|
101
|
+
}
|
|
102
|
+
this.debug && console.log(`MemoryStore<${this.key}>.scan`, { keys });
|
|
103
|
+
return keys;
|
|
104
|
+
}
|
|
105
|
+
async ids(query = {}) {
|
|
106
|
+
this.debug && console.log(`MemoryStore<${this.key}>.ids`, { query });
|
|
107
|
+
if (query.scan) {
|
|
108
|
+
return this.scan(query);
|
|
109
|
+
}
|
|
110
|
+
let count = query.count;
|
|
111
|
+
delete query.count;
|
|
112
|
+
if (typeof (count) != "undefined" && typeof (count) != "number") {
|
|
113
|
+
console.warn(`MemoryStore<${this.key}>.ids WARNING: query.count is not a number`);
|
|
114
|
+
count = undefined;
|
|
115
|
+
}
|
|
116
|
+
let offset = query.offset;
|
|
117
|
+
delete query.offset;
|
|
118
|
+
if (typeof (offset) != "undefined" && typeof (offset) != "number") {
|
|
119
|
+
console.warn(`MemoryStore<${this.key}>.ids WARNING: query.offset must be a number`);
|
|
120
|
+
offset = undefined;
|
|
121
|
+
}
|
|
122
|
+
const min = offset || 0;
|
|
123
|
+
const max = min + (count || 0) - 1;
|
|
124
|
+
const queryEntries = query && Object.entries(query);
|
|
125
|
+
if (!(queryEntries === null || queryEntries === void 0 ? void 0 : queryEntries.length)) {
|
|
126
|
+
return new Set(this.index.range(min, max, true));
|
|
127
|
+
}
|
|
128
|
+
const setOfIds = queryEntries.map(([queryKey, queryVal]) => {
|
|
129
|
+
var _a;
|
|
130
|
+
if (queryVal) {
|
|
131
|
+
const lookupKey = `${this.setKey}:${queryKey}:${queryVal}`;
|
|
132
|
+
return new Set(((_a = this.lookups.get(lookupKey)) === null || _a === void 0 ? void 0 : _a.range(min, max, true)) || []);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
throw `MemoryStore.ids(query) query must have key and value`;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
const ids = setOfIds.reduce((prev, curr) => intersect(curr, prev || curr), undefined) || new Set();
|
|
139
|
+
this.debug && console.log(`MemoryStore<${this.key}>.ids queried lookup key`, { query, setOfIds, ids });
|
|
140
|
+
return ids;
|
|
141
|
+
}
|
|
142
|
+
async find(query = {}) {
|
|
143
|
+
this.debug && console.log(`MemoryStore<${this.key}>.find`, { query });
|
|
144
|
+
const ids = Array.isArray(query.id)
|
|
145
|
+
? query.id.filter(Boolean)
|
|
146
|
+
: Array.from(await this.ids(query));
|
|
147
|
+
const values = ids
|
|
148
|
+
.map((id) => this.records.get(id))
|
|
149
|
+
.filter((value) => !!value && !value.deletedAt);
|
|
150
|
+
this.debug && console.log(`MemoryStore<${this.key}>.find`, { values });
|
|
151
|
+
return values;
|
|
152
|
+
}
|
|
153
|
+
async create(value, options) {
|
|
154
|
+
this.debug && console.log(`MemoryStore<${this.key}>.create`, { value, options, this_options: this.options });
|
|
155
|
+
const now = Date.now();
|
|
156
|
+
options = { ...this.options, ...options };
|
|
157
|
+
const createdValue = {
|
|
158
|
+
id: value.id || (0, utils_1.uuid)(),
|
|
159
|
+
createdAt: value.createdAt || now,
|
|
160
|
+
...value,
|
|
161
|
+
};
|
|
162
|
+
this.debug && console.log(`MemoryStore<${this.key}>.create`, { createdValue });
|
|
163
|
+
this._index(createdValue, options);
|
|
164
|
+
(options === null || options === void 0 ? void 0 : options.expire) && this.debug && console.log(`MemoryStore<${this.key}>.create ignoring options.expire (memory store is ephemeral)`);
|
|
165
|
+
return createdValue;
|
|
166
|
+
}
|
|
167
|
+
async update(value, options) {
|
|
168
|
+
this.debug && console.log(`MemoryStore<${this.key}>.update`, { value, options });
|
|
169
|
+
if (!value.id) {
|
|
170
|
+
throw `Cannot update ${this.key}: null id`;
|
|
171
|
+
}
|
|
172
|
+
const prevValue = await this.get(value.id);
|
|
173
|
+
if (!prevValue) {
|
|
174
|
+
throw `Cannot update ${this.key}: does not exist: ${value.id}`;
|
|
175
|
+
}
|
|
176
|
+
const now = Date.now();
|
|
177
|
+
options = { ...this.options, ...options };
|
|
178
|
+
const updatedValue = {
|
|
179
|
+
...value,
|
|
180
|
+
updatedAt: now,
|
|
181
|
+
};
|
|
182
|
+
// @ts-ignore
|
|
183
|
+
const prevLookupKeys = new Map(this.lookupKeys(prevValue, options));
|
|
184
|
+
// @ts-ignore
|
|
185
|
+
const lookupKeys = new Map(this.lookupKeys(updatedValue, options));
|
|
186
|
+
const lookupsToRemove = prevLookupKeys && Array.from(prevLookupKeys)
|
|
187
|
+
.filter(([k, v]) => !lookupKeys || lookupKeys.get(k) != v);
|
|
188
|
+
const lookupsToAdd = lookupKeys && Array.from(lookupKeys)
|
|
189
|
+
.filter(([k, v]) => !prevLookupKeys || prevLookupKeys.get(k) != v);
|
|
190
|
+
this.debug && console.log(`MemoryStore<${this.key}>.update`, { prevLookupKeys, lookupKeys, keysToRemove: lookupsToRemove, keysToAdd: lookupsToAdd });
|
|
191
|
+
lookupsToRemove.forEach(([lookupKey, id]) => { var _a; return (_a = this.lookups.get(lookupKey)) === null || _a === void 0 ? void 0 : _a.remove(id); });
|
|
192
|
+
this.records.set(value.id, updatedValue);
|
|
193
|
+
lookupsToAdd.forEach(([lookupKey, id]) => {
|
|
194
|
+
if (!this.lookups.has(lookupKey))
|
|
195
|
+
this.lookups.set(lookupKey, new SortedIdList());
|
|
196
|
+
this.lookups.get(lookupKey).add(id, updatedValue.createdAt || updatedValue.updatedAt);
|
|
197
|
+
});
|
|
198
|
+
return updatedValue;
|
|
199
|
+
}
|
|
200
|
+
async incCounters(values, delta) {
|
|
201
|
+
var _a;
|
|
202
|
+
this.debug && console.log(`MemoryStore<${this.key}>.incCounters`, { values, delta });
|
|
203
|
+
const counters = ((_a = this.options) === null || _a === void 0 ? void 0 : _a.counters) || [];
|
|
204
|
+
counters
|
|
205
|
+
.filter((counter) => !counter.split(":").every((d) => typeof values[d] != "undefined"))
|
|
206
|
+
.forEach((counter) => {
|
|
207
|
+
const missing = counter.split(":").filter((d) => typeof values[d] == "undefined");
|
|
208
|
+
console.warn(`MemoryStore<${this.key}>.incCounters WARNING: skipping counter "${counter}": missing dimension(s) ${missing.join(", ")} in values`, { values });
|
|
209
|
+
});
|
|
210
|
+
counters
|
|
211
|
+
.filter((counter) => counter.split(":").every((d) => typeof values[d] != "undefined"))
|
|
212
|
+
.forEach((counter) => {
|
|
213
|
+
const member = counter.split(":").map((d) => `${d}=${values[d]}`).join(":");
|
|
214
|
+
[
|
|
215
|
+
[`${this.key}Totals:${counter}`, delta.total],
|
|
216
|
+
[`${this.key}Counts:${counter}`, delta.count],
|
|
217
|
+
].forEach(([setKey, d]) => {
|
|
218
|
+
if (!this.counters.has(setKey))
|
|
219
|
+
this.counters.set(setKey, new Map());
|
|
220
|
+
const counterMap = this.counters.get(setKey);
|
|
221
|
+
counterMap.set(member, (counterMap.get(member) || 0) + d);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
async queryCounter(kind, counter, exact, range) {
|
|
226
|
+
this.debug && console.log(`MemoryStore<${this.key}>.queryCounter`, { kind, counter, exact, range });
|
|
227
|
+
const dims = counter.split(":");
|
|
228
|
+
const setKey = `${this.key}${kind == "totals" ? "Totals" : "Counts"}:${counter}`;
|
|
229
|
+
const counterMap = this.counters.get(setKey) || new Map();
|
|
230
|
+
const prefix = dims
|
|
231
|
+
.filter((d) => typeof exact[d] != "undefined")
|
|
232
|
+
.map((d) => `${d}=${exact[d]}`)
|
|
233
|
+
.join(":");
|
|
234
|
+
const hasMore = dims.length > Object.keys(exact).length;
|
|
235
|
+
const minPrefix = (range === null || range === void 0 ? void 0 : range.min) != null ? `${prefix}${prefix ? ":" : ""}${range.field}=${range.min}` : prefix;
|
|
236
|
+
const maxPrefix = (range === null || range === void 0 ? void 0 : range.max) != null ? `${prefix}${prefix ? ":" : ""}${range.field}=${range.max}` : prefix;
|
|
237
|
+
// matches RedisStore's `[`-prefixed (always inclusive) ZRANGEBYLEX bounds, minus the
|
|
238
|
+
// Redis-specific bound-type marker -- comparisons below are plain string comparisons.
|
|
239
|
+
const min = `${minPrefix}${hasMore ? ":\x00" : ""}`;
|
|
240
|
+
const max = `${maxPrefix}${hasMore ? ":\x7f" : ""}`;
|
|
241
|
+
this.debug && console.log(`MemoryStore<${this.key}>.queryCounter`, { setKey, min, max });
|
|
242
|
+
// sorted lexicographically to mirror ZRANGEBYLEX's byte-order member comparison
|
|
243
|
+
const members = Array.from(counterMap.keys()).sort();
|
|
244
|
+
const filtered = members.filter((m) => m >= min && m <= max);
|
|
245
|
+
if (kind == "count") {
|
|
246
|
+
this.debug && console.log(`MemoryStore<${this.key}>.queryCounter`, { count: filtered.length });
|
|
247
|
+
return filtered.length;
|
|
248
|
+
}
|
|
249
|
+
const results = filtered.map((member) => ({ member, score: counterMap.get(member) }));
|
|
250
|
+
this.debug && console.log(`MemoryStore<${this.key}>.queryCounter`, { results });
|
|
251
|
+
return results;
|
|
252
|
+
}
|
|
253
|
+
async delete(id, options) {
|
|
254
|
+
this.debug && console.log(`MemoryStore<${this.key}>.delete`, { id, options });
|
|
255
|
+
if (!id) {
|
|
256
|
+
throw `Cannot delete ${this.key}: null id`;
|
|
257
|
+
}
|
|
258
|
+
options = { ...this.options, ...options };
|
|
259
|
+
const value = await this.get(id, { deleted: true });
|
|
260
|
+
if (!value) {
|
|
261
|
+
console.warn(`MemoryStore<${this.key}>.delete WARNING: does not exist: ${id}`);
|
|
262
|
+
}
|
|
263
|
+
const lookupKeys = value && this.lookupKeys(value, options);
|
|
264
|
+
this.debug && console.log(`MemoryStore<${this.key}>.delete`, { lookupKeys });
|
|
265
|
+
const deletedAt = Date.now();
|
|
266
|
+
if (value) {
|
|
267
|
+
if (options === null || options === void 0 ? void 0 : options.hardDelete) {
|
|
268
|
+
this.records.delete(id);
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
this.records.set(id, { ...value, deletedAt });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
this.index.remove(id);
|
|
275
|
+
(lookupKeys || []).forEach(([lookupKey, lookupId]) => { var _a; return (_a = this.lookups.get(lookupKey)) === null || _a === void 0 ? void 0 : _a.remove(lookupId); });
|
|
276
|
+
return value ? { ...value, deletedAt } : undefined;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
exports.default = MemoryStore;
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { RedisStoreRecord } from "./index";
|
|
2
|
+
export interface Store<T extends RedisStoreRecord> {
|
|
3
|
+
key: string;
|
|
4
|
+
setKey: string;
|
|
5
|
+
options: any;
|
|
6
|
+
debug: boolean;
|
|
7
|
+
exists(id: string): Promise<boolean>;
|
|
8
|
+
get(id: string, options?: any): Promise<T | undefined>;
|
|
9
|
+
scan(query?: any): Promise<Set<string>>;
|
|
10
|
+
ids(query?: any): Promise<Set<string>>;
|
|
11
|
+
find(query?: any): Promise<T[]>;
|
|
12
|
+
create(value: any, options?: {
|
|
13
|
+
expire?: number;
|
|
14
|
+
noIndex?: boolean;
|
|
15
|
+
score?: number;
|
|
16
|
+
noLookup?: boolean;
|
|
17
|
+
lookups?: any;
|
|
18
|
+
}): Promise<T>;
|
|
19
|
+
update(value: any, options?: any): Promise<T>;
|
|
20
|
+
incCounters(values: Record<string, string | number>, delta: {
|
|
21
|
+
total: number;
|
|
22
|
+
count: number;
|
|
23
|
+
}): Promise<any>;
|
|
24
|
+
queryCounter(kind: "count" | "counts" | "totals", counter: string, exact: Record<string, string | number>, range?: {
|
|
25
|
+
field: string;
|
|
26
|
+
min?: string;
|
|
27
|
+
max?: string;
|
|
28
|
+
}): Promise<number | Array<{
|
|
29
|
+
member: string;
|
|
30
|
+
score: number;
|
|
31
|
+
}>>;
|
|
32
|
+
delete(id: string, options?: {
|
|
33
|
+
hardDelete?: boolean;
|
|
34
|
+
noLookup?: boolean;
|
|
35
|
+
lookups?: any;
|
|
36
|
+
}): Promise<T | undefined>;
|
|
37
|
+
}
|
package/dist/store.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@desmat/redis-store",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
15
|
"build": "tsc",
|
|
16
|
+
"prepare": "tsc",
|
|
16
17
|
"example": "ts-node --skipProject ./src/example.ts"
|
|
17
18
|
},
|
|
18
19
|
"repository": {
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
"license": "MIT",
|
|
31
32
|
"dependencies": {
|
|
32
33
|
"@desmat/utils": "^1.0.0",
|
|
33
|
-
"@upstash/redis": "^1.
|
|
34
|
+
"@upstash/redis": "^1.38.0",
|
|
34
35
|
"dotenv": "^16.4.5"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|