@desmat/redis-store 1.0.0 → 1.0.2
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/README.md +159 -4
- package/dist/example.d.ts +1 -0
- package/dist/example.js +89 -0
- package/dist/index.d.ts +8 -8
- package/dist/index.js +23 -20
- package/dist/repl.d.ts +1 -0
- package/dist/repl.js +139 -0
- package/dist/simple-example.d.ts +1 -0
- package/dist/simple-example.js +94 -0
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -1,5 +1,160 @@
|
|
|
1
|
-
# redis-store
|
|
2
|
-
A lightweight wrapper for using Redis as a data store
|
|
1
|
+
# @desmat/redis-store
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
A lightweight library to facilitate using the (in)famously fast in-memory database as your primary data store for your app’s entities and their relationships.
|
|
4
|
+
|
|
5
|
+
Leans into Redis’ strong suits to bring relational aspects to a simple but performant KV store:
|
|
6
|
+
- Lots of small read/writes
|
|
7
|
+
- JSON keys for storing entities (no migration scripts required)
|
|
8
|
+
- ZSET keys to track lists and relations
|
|
9
|
+
|
|
10
|
+
Plays well with Upstash and Vercel but any Redis instance supported via REST API.
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## Getting Started
|
|
14
|
+
|
|
15
|
+
Below shows a simple schema with users and things belonging to users, some data added then queried.
|
|
16
|
+
|
|
17
|
+
`npm run example` to run [example.ts](./src/example.ts).
|
|
18
|
+
|
|
19
|
+
### Install the library into your project
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @desmat/redis-store
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Setup environment variables
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# your .env file, or provided in launch command
|
|
29
|
+
KV_REST_API_URL=*****
|
|
30
|
+
KV_REST_API_TOKEN=*****
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
*Note: Using Vercel KV environment variables enables this library to be used without friction on Vercel's platform, but `url` and `token` values can be provided in code.*
|
|
34
|
+
|
|
35
|
+
### Setup entities and store
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import RedisStore from '@desmat/redis-store';
|
|
39
|
+
|
|
40
|
+
type User = {
|
|
41
|
+
id: string, // required; set by .create as short UUID by default, or can be provided
|
|
42
|
+
createdAt: number, // required; set by .create
|
|
43
|
+
name: string,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type Thing = {
|
|
47
|
+
id: string,
|
|
48
|
+
createdAt: number,
|
|
49
|
+
createdBy: string, // required to lookup Things by Users
|
|
50
|
+
label: string,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const ThingOptions = {
|
|
54
|
+
lookups: {
|
|
55
|
+
user: "createdBy", // enables .find({ user: <USERID> })
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// with environment variables `KV_REST_API_URL` and `KV_REST_API_TOKEN`
|
|
60
|
+
// otherwise `url` and `token` can be provided to `RedisStore` constructor
|
|
61
|
+
const store = {
|
|
62
|
+
users: new RedisStore<User>({ key: "user" }),
|
|
63
|
+
things: new RedisStore<Thing>({ key: "thing", options: ThingOptions }),
|
|
64
|
+
};
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Create users and things
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
const users = await Promise.all([
|
|
71
|
+
store.users.create({ name: "User One" }),
|
|
72
|
+
store.users.create({ name: "User Two" }),
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
// users (2): [
|
|
76
|
+
// { id: '<UUID>', createdAt: <TIMEINMILLIS>, name: 'User One' },
|
|
77
|
+
// ...
|
|
78
|
+
// ]
|
|
79
|
+
|
|
80
|
+
// Redis commands executed:
|
|
81
|
+
//
|
|
82
|
+
// JSON.SET user:<UUID> $ '{ "id": "<UUID>", "createdAt": <TIMEINMILLIS>, "name": "User One" }'
|
|
83
|
+
// ZADD users <TIMEINMILLIS> <UUID>
|
|
84
|
+
// ...
|
|
85
|
+
|
|
86
|
+
const things = await Promise.all([
|
|
87
|
+
store.things.create({
|
|
88
|
+
createdBy: users[0].id,
|
|
89
|
+
label: "A thing for user one",
|
|
90
|
+
}),
|
|
91
|
+
store.things.create({
|
|
92
|
+
createdBy: users[0].id,
|
|
93
|
+
label: "Another thing for user one",
|
|
94
|
+
}),
|
|
95
|
+
store.things.create({
|
|
96
|
+
createdBy: users[0].id,
|
|
97
|
+
label: "Yet another thing for user one",
|
|
98
|
+
}),
|
|
99
|
+
store.things.create({
|
|
100
|
+
createdBy: users[1].id,
|
|
101
|
+
label: "A thing for user two",
|
|
102
|
+
}),
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
// things (4): [
|
|
106
|
+
// {
|
|
107
|
+
// id: '<UUID>',
|
|
108
|
+
// createdAt: <TIMEINMILLIS>,
|
|
109
|
+
// createdBy: '<USER_UUID>',
|
|
110
|
+
// label: 'A thing for user one'
|
|
111
|
+
// },
|
|
112
|
+
// ...
|
|
113
|
+
// ]
|
|
114
|
+
|
|
115
|
+
// Redis commands executed:
|
|
116
|
+
//
|
|
117
|
+
// JSON.SET thing:<UUID> $ '{ "id": "<UUID>", "createdAt": <TIMEINMILLIS>, "createdBy": "<USER_UUID>", "message": "Another thing for user one" }'
|
|
118
|
+
// ZADD things <TIMEINMILLIS> <UUID>
|
|
119
|
+
// ZADD things:user:<USER_UUID> <TIMEINMILLIS> <UUID>
|
|
120
|
+
// ...
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Query things
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
const allThings = await store.things.find(); // 4 entries
|
|
127
|
+
|
|
128
|
+
// Redis commands executed:
|
|
129
|
+
//
|
|
130
|
+
// ZRANGE things:user:<USER_UUID> 0 -1 REV
|
|
131
|
+
// JSON.MGET thing:<THING1_UUID> thing:<THING2_UUID> ...
|
|
132
|
+
|
|
133
|
+
const latestUserThings = await store.things.find({
|
|
134
|
+
user: users[0].id
|
|
135
|
+
}); // 3 entries
|
|
136
|
+
|
|
137
|
+
// Redis commands executed:
|
|
138
|
+
//
|
|
139
|
+
// ZRANGE things:user:<UUID> 0 -1 REV
|
|
140
|
+
// JSON.MGET thing:<THING1_UUID> thing:<THING2_UUID> thing:<THING3_UUID>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Cleanup (soft delete by default)
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
await Promise.all([
|
|
147
|
+
...users.map((user: User) => store.users.delete(user.id)),
|
|
148
|
+
...things.map((thing: Thing) => store.things.delete(thing.id)),
|
|
149
|
+
]);
|
|
150
|
+
|
|
151
|
+
// Redis commands executed:
|
|
152
|
+
//
|
|
153
|
+
// JSON.SET user:<UUID> $.deletedAt <TIMEINMILLIS>
|
|
154
|
+
// ZREM users <UUID>
|
|
155
|
+
// ...
|
|
156
|
+
// JSON.SET thing:<UUID> $.deletedAt <TIMEINMILLIS>
|
|
157
|
+
// ZREM things <UUID>
|
|
158
|
+
// ZREM testthings:user:<USER_UUID> <UUID>
|
|
159
|
+
// ...
|
|
160
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/example.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const index_1 = __importDefault(require("./index"));
|
|
7
|
+
require('dotenv').config();
|
|
8
|
+
const ThingOptions = {
|
|
9
|
+
lookups: {
|
|
10
|
+
user: "createdBy", // enable .find({ user: <USERID> })
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
// with environment variables KV_REST_API_URL and KV_REST_API_TOKEN
|
|
14
|
+
// or `url` and `token` keys provided to RedisStore constructor
|
|
15
|
+
const store = {
|
|
16
|
+
users: new index_1.default({ key: "user" }),
|
|
17
|
+
things: new index_1.default({ key: "thing", options: ThingOptions }),
|
|
18
|
+
};
|
|
19
|
+
(async function () {
|
|
20
|
+
// create users
|
|
21
|
+
const users = await Promise.all([
|
|
22
|
+
store.users.create({ name: "User One" }),
|
|
23
|
+
store.users.create({ name: "User Two" }),
|
|
24
|
+
]);
|
|
25
|
+
// Redis commands executed:
|
|
26
|
+
// JSON.SET user:<UUID> $ '{ "id": "<UUID>", "createdAt": <TIMEINMILLIS>, "name": "User One" }'
|
|
27
|
+
// ZADD users <TIMEINMILLIS> <UUID>
|
|
28
|
+
// ...
|
|
29
|
+
console.log("users", users);
|
|
30
|
+
// users (2 entries): [
|
|
31
|
+
// { id: '<UUID>', createdAt: <TIMEINMILLIS>, name: 'User One' },
|
|
32
|
+
// ...
|
|
33
|
+
// ]
|
|
34
|
+
// create things
|
|
35
|
+
const things = await Promise.all([
|
|
36
|
+
store.things.create({
|
|
37
|
+
createdBy: users[0].id,
|
|
38
|
+
label: "A thing for user one",
|
|
39
|
+
}),
|
|
40
|
+
store.things.create({
|
|
41
|
+
createdBy: users[0].id,
|
|
42
|
+
label: "Another thing for user one",
|
|
43
|
+
}),
|
|
44
|
+
store.things.create({
|
|
45
|
+
createdBy: users[0].id,
|
|
46
|
+
label: "Yet another thing for user one",
|
|
47
|
+
}),
|
|
48
|
+
store.things.create({
|
|
49
|
+
createdBy: users[1].id,
|
|
50
|
+
label: "A thing for user two",
|
|
51
|
+
}),
|
|
52
|
+
]);
|
|
53
|
+
// JSON.SET thing:<UUID> $ '{ "id": "<UUID>", "createdAt": <TIMEINMILLIS>, "createdBy": "<USER_UUID>", "message": "Another thing for user one" }'
|
|
54
|
+
// ZADD things <TIMEINMILLIS> <UUID>
|
|
55
|
+
// ZADD things:user:<USER_UUID> <TIMEINMILLIS> <UUID>
|
|
56
|
+
// ...
|
|
57
|
+
console.log("things", things);
|
|
58
|
+
// things (4 entries): [
|
|
59
|
+
// {
|
|
60
|
+
// id: '<UUID>',
|
|
61
|
+
// createdAt: <TIMEINMILLIS>,
|
|
62
|
+
// createdBy: '<USER_UUID>',
|
|
63
|
+
// label: 'A thing for user one'
|
|
64
|
+
// },
|
|
65
|
+
// ...
|
|
66
|
+
// ]
|
|
67
|
+
// all things
|
|
68
|
+
const allThings = await store.things.find();
|
|
69
|
+
// ZRANGE things:user:<USER_UUID> 0 -1 REV
|
|
70
|
+
// JSON.MGET thing:<THING1_UUID> thing:<THING2_UUID> ...
|
|
71
|
+
console.log("allThings", allThings); // 4 entries
|
|
72
|
+
// latest things from first user
|
|
73
|
+
const latestUserThings = await store.things.find({ user: users[0].id });
|
|
74
|
+
// ZRANGE things:user:<USER_UUID> 0 -1 REV
|
|
75
|
+
// JSON.MGET thing:<THING1_UUID> thing:<THING2_UUID> thing:<THING3_UUID>
|
|
76
|
+
console.log("latestUserThings", latestUserThings); // 3 entries
|
|
77
|
+
// cleanup from this session (soft delete by default)
|
|
78
|
+
await Promise.all([
|
|
79
|
+
...users.map((user) => store.users.delete(user.id)),
|
|
80
|
+
...things.map((thing) => store.things.delete(thing.id)),
|
|
81
|
+
]);
|
|
82
|
+
// JSON.SET user:<UUID> $.deletedAt <TIMEINMILLIS>
|
|
83
|
+
// ZREM users <UUID>
|
|
84
|
+
// ...
|
|
85
|
+
// JSON.SET thing:<UUID> $.deletedAt <TIMEINMILLIS>
|
|
86
|
+
// ZREM things <UUID>
|
|
87
|
+
// ZREM testthings:user:<USER_UUID> <UUID>
|
|
88
|
+
// ...
|
|
89
|
+
})();
|
package/dist/index.d.ts
CHANGED
|
@@ -5,28 +5,28 @@ export type RedisStoreRecord = {
|
|
|
5
5
|
updatedAt?: number;
|
|
6
6
|
deletedAt?: number;
|
|
7
7
|
};
|
|
8
|
-
export
|
|
8
|
+
export default class RedisStore<T extends RedisStoreRecord> {
|
|
9
9
|
redis: Redis;
|
|
10
10
|
key: string;
|
|
11
11
|
setKey: string;
|
|
12
12
|
valueKey: (id: string) => string;
|
|
13
|
-
|
|
13
|
+
options: any;
|
|
14
14
|
debug: boolean;
|
|
15
|
-
constructor({ url, token, key, setKey,
|
|
16
|
-
url: string;
|
|
17
|
-
token: string;
|
|
15
|
+
constructor({ url, token, key, setKey, options, debug, }: {
|
|
18
16
|
key: string;
|
|
19
17
|
setKey?: string;
|
|
20
|
-
|
|
18
|
+
options?: any;
|
|
19
|
+
url?: string;
|
|
20
|
+
token?: string;
|
|
21
21
|
debug?: boolean;
|
|
22
22
|
});
|
|
23
23
|
lookupKeys(value: any, options?: any): any[][];
|
|
24
24
|
exists(id: string): Promise<boolean>;
|
|
25
|
-
get(id: string): Promise<T | undefined>;
|
|
25
|
+
get(id: string, options?: any): Promise<T | undefined>;
|
|
26
26
|
scan(query?: any): Promise<Set<string>>;
|
|
27
27
|
ids(query?: any): Promise<Set<string>>;
|
|
28
28
|
find(query?: any): Promise<T[]>;
|
|
29
29
|
create(value: any, options?: any): Promise<T>;
|
|
30
30
|
update(value: any, options?: any): Promise<T>;
|
|
31
|
-
delete(id: string, options?: any): Promise<T>;
|
|
31
|
+
delete(id: string, options?: any): Promise<T | undefined>;
|
|
32
32
|
}
|
package/dist/index.js
CHANGED
|
@@ -19,21 +19,23 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
19
19
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
20
20
|
};
|
|
21
21
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
-
exports.RedisStore = void 0;
|
|
23
22
|
const moment_1 = __importDefault(require("moment"));
|
|
24
23
|
const utils_1 = require("@desmat/utils");
|
|
25
24
|
const redis_1 = require("@upstash/redis");
|
|
26
25
|
class RedisStore {
|
|
27
|
-
constructor({ url, token, key, setKey,
|
|
28
|
-
this.redis = new redis_1.Redis({
|
|
26
|
+
constructor({ url, token, key, setKey, options, debug, }) {
|
|
27
|
+
this.redis = new redis_1.Redis({
|
|
28
|
+
url: url || process.env.KV_REST_API_URL,
|
|
29
|
+
token: token || process.env.KV_REST_API_TOKEN
|
|
30
|
+
});
|
|
29
31
|
this.key = key;
|
|
30
32
|
this.setKey = setKey || key + "s";
|
|
31
33
|
this.valueKey = (id) => `${key}:${id}`;
|
|
32
|
-
this.
|
|
34
|
+
this.options = options;
|
|
33
35
|
this.debug = !!debug;
|
|
34
36
|
}
|
|
35
37
|
lookupKeys(value, options) {
|
|
36
|
-
options = { ...this.
|
|
38
|
+
options = { ...this.options, ...options };
|
|
37
39
|
this.debug && console.log(`RedisStore.lookupKeys<${this.key}>.lookupKeys`, { value, options });
|
|
38
40
|
/*
|
|
39
41
|
create index and lookup sets based on options.lookups
|
|
@@ -82,19 +84,20 @@ class RedisStore {
|
|
|
82
84
|
this.debug && console.log(`RedisStore<${this.key}>.exists`, { response });
|
|
83
85
|
return response > 0;
|
|
84
86
|
}
|
|
85
|
-
async get(id) {
|
|
87
|
+
async get(id, options) {
|
|
86
88
|
this.debug && console.log(`RedisStore<${this.key}>.get`, { id });
|
|
87
89
|
const response = await this.redis.json.get(this.valueKey(id), "$");
|
|
88
90
|
this.debug && console.log(`RedisStore<${this.key}>.get`, { response });
|
|
89
91
|
let value;
|
|
90
|
-
if (response && response[0] && !response[0].deletedAt) {
|
|
92
|
+
if (response && response[0] && !(response[0].deletedAt && !(options === null || options === void 0 ? void 0 : options.deleted))) {
|
|
91
93
|
value = response[0];
|
|
92
94
|
}
|
|
93
95
|
return value;
|
|
94
96
|
}
|
|
95
97
|
async scan(query = {}) {
|
|
96
98
|
this.debug && console.log(`RedisStore<${this.key}>.scan`, { query });
|
|
97
|
-
|
|
99
|
+
!query.count && console.warn(`RedisStore.RedisStore<${this.key}>.find WARNING: scan command with no count provided: setting count at 999`);
|
|
100
|
+
const count = query.count || 999;
|
|
98
101
|
const match = this.valueKey(query.scan);
|
|
99
102
|
let keys = new Set();
|
|
100
103
|
let nextCursor = "0";
|
|
@@ -170,7 +173,7 @@ class RedisStore {
|
|
|
170
173
|
this.debug && console.log(`RedisStore<${this.key}>.find`, { keys });
|
|
171
174
|
}
|
|
172
175
|
// don't mget too many at once otherwise 💥
|
|
173
|
-
const blockSize =
|
|
176
|
+
const blockSize = 256;
|
|
174
177
|
const blocks = keys && keys.length && Array
|
|
175
178
|
.apply(null, Array(Math.ceil(keys.length / blockSize)))
|
|
176
179
|
.map((v, block) => (keys || [])
|
|
@@ -187,9 +190,9 @@ class RedisStore {
|
|
|
187
190
|
return values;
|
|
188
191
|
}
|
|
189
192
|
async create(value, options) {
|
|
190
|
-
this.debug && console.log(`RedisStore<${this.key}>.create`, { value, options,
|
|
193
|
+
this.debug && console.log(`RedisStore<${this.key}>.create`, { value, options, this_options: this.options });
|
|
191
194
|
const now = (0, moment_1.default)().valueOf();
|
|
192
|
-
options = { ...this.
|
|
195
|
+
options = { ...this.options, ...options };
|
|
193
196
|
const createdValue = {
|
|
194
197
|
id: value.id || (0, utils_1.uuid)(),
|
|
195
198
|
createdAt: value.createdAt || now,
|
|
@@ -217,7 +220,7 @@ class RedisStore {
|
|
|
217
220
|
throw `Cannot update ${this.key}: does not exist: ${value.id}`;
|
|
218
221
|
}
|
|
219
222
|
const now = (0, moment_1.default)().valueOf();
|
|
220
|
-
options = { ...this.
|
|
223
|
+
options = { ...this.options, ...options };
|
|
221
224
|
const updatedValue = {
|
|
222
225
|
...value,
|
|
223
226
|
updatedAt: now,
|
|
@@ -252,23 +255,23 @@ class RedisStore {
|
|
|
252
255
|
if (!id) {
|
|
253
256
|
throw `Cannot delete ${this.key}: null id`;
|
|
254
257
|
}
|
|
255
|
-
options = { ...this.
|
|
256
|
-
const value = await this.get(id);
|
|
258
|
+
options = { ...this.options, ...options };
|
|
259
|
+
const value = await this.get(id, { deleted: true });
|
|
257
260
|
if (!value) {
|
|
258
|
-
|
|
261
|
+
console.warn(`RedisStore<${this.key}>.delete WARNING: does not exist: ${id}`);
|
|
259
262
|
}
|
|
260
|
-
const lookupKeys = this.lookupKeys(value, options);
|
|
263
|
+
const lookupKeys = value && this.lookupKeys(value, options);
|
|
261
264
|
this.debug && console.log(`RedisStore<${this.key}>.delete`, { lookupKeys });
|
|
262
|
-
|
|
265
|
+
const deletedAt = (0, moment_1.default)().valueOf();
|
|
263
266
|
const response = await Promise.all([
|
|
264
267
|
options.hardDelete
|
|
265
268
|
? this.redis.json.del(this.valueKey(id), "$")
|
|
266
|
-
: this.redis.json.set(this.valueKey(id), "
|
|
269
|
+
: this.redis.json.set(this.valueKey(id), "$.deletedAt", deletedAt),
|
|
267
270
|
this.redis.zrem(this.setKey, id),
|
|
268
271
|
...(lookupKeys ? lookupKeys.map((lookupKey) => this.redis.zrem(lookupKey[0], lookupKey[1])) : []),
|
|
269
272
|
]);
|
|
270
273
|
this.debug && console.log(`RedisStore<${this.key}>.delete`, { response });
|
|
271
|
-
return value;
|
|
274
|
+
return value ? { ...value, deletedAt } : undefined;
|
|
272
275
|
}
|
|
273
276
|
}
|
|
274
|
-
exports.
|
|
277
|
+
exports.default = RedisStore;
|
package/dist/repl.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/repl.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const moment_1 = __importDefault(require("moment"));
|
|
7
|
+
const index_1 = require("./index");
|
|
8
|
+
const KV_REST_API_URL = "https://close-ringtail-23695.upstash.io";
|
|
9
|
+
const KV_REST_API_TOKEN = "AVyPAAIjcDE4ZDhiZjAxNjU3NjI0NGU1YjIwMzAzNzkzMWRkMDU2ZHAxMA";
|
|
10
|
+
const debug = false;
|
|
11
|
+
const UserRecordOptions = {
|
|
12
|
+
lookups: {
|
|
13
|
+
// user: "createdBy",
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
const PostRecordOptions = {
|
|
17
|
+
lookups: {
|
|
18
|
+
user: "createdBy",
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
const UserPostRecordOptions = {
|
|
22
|
+
lookups: {
|
|
23
|
+
user: "userId",
|
|
24
|
+
post: "postId",
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
const store = {
|
|
28
|
+
users: new index_1.RedisStore({
|
|
29
|
+
url: KV_REST_API_URL,
|
|
30
|
+
token: KV_REST_API_TOKEN,
|
|
31
|
+
key: "testuser",
|
|
32
|
+
setKey: "testusers",
|
|
33
|
+
recordOptions: UserRecordOptions,
|
|
34
|
+
debug,
|
|
35
|
+
}),
|
|
36
|
+
posts: new index_1.RedisStore({
|
|
37
|
+
url: KV_REST_API_URL,
|
|
38
|
+
token: KV_REST_API_TOKEN,
|
|
39
|
+
key: "testpost",
|
|
40
|
+
setKey: "testposts",
|
|
41
|
+
recordOptions: PostRecordOptions,
|
|
42
|
+
debug,
|
|
43
|
+
}),
|
|
44
|
+
userPosts: new index_1.RedisStore({
|
|
45
|
+
url: KV_REST_API_URL,
|
|
46
|
+
token: KV_REST_API_TOKEN,
|
|
47
|
+
key: "testuserpost",
|
|
48
|
+
setKey: "testuserposts",
|
|
49
|
+
recordOptions: UserPostRecordOptions,
|
|
50
|
+
debug,
|
|
51
|
+
}),
|
|
52
|
+
};
|
|
53
|
+
(async function () {
|
|
54
|
+
// console.log("Hello!");
|
|
55
|
+
// new user signs up
|
|
56
|
+
const users = await Promise.all([
|
|
57
|
+
store.users.create({ name: "User One" }),
|
|
58
|
+
store.users.create({ name: "User Two" }),
|
|
59
|
+
store.users.create({ name: "User Three" }),
|
|
60
|
+
]);
|
|
61
|
+
// users creates posts
|
|
62
|
+
const posts = await Promise.all([
|
|
63
|
+
store.posts.create({
|
|
64
|
+
createdBy: users[0].id,
|
|
65
|
+
message: "A post by user one",
|
|
66
|
+
}),
|
|
67
|
+
store.posts.create({
|
|
68
|
+
createdBy: users[0].id,
|
|
69
|
+
message: "Another post by user one",
|
|
70
|
+
}),
|
|
71
|
+
store.posts.create({
|
|
72
|
+
createdBy: users[0].id,
|
|
73
|
+
message: "Yet another post by user one",
|
|
74
|
+
}),
|
|
75
|
+
store.posts.create({
|
|
76
|
+
createdBy: users[1].id,
|
|
77
|
+
message: "A post by user two",
|
|
78
|
+
}),
|
|
79
|
+
]);
|
|
80
|
+
// show latest posts from first user
|
|
81
|
+
const latestPosts = await store.posts.find({ user: users[0].id, count: 3 });
|
|
82
|
+
console.log("=== Latest posts ===");
|
|
83
|
+
latestPosts.forEach((post) => {
|
|
84
|
+
console.log(`[post ${post.id}]: ${post.message}`);
|
|
85
|
+
});
|
|
86
|
+
// users views posts
|
|
87
|
+
const userPosts = await Promise.all([
|
|
88
|
+
store.userPosts.create({
|
|
89
|
+
id: `${users[1].id}:${posts[0].id}`,
|
|
90
|
+
userId: users[1].id,
|
|
91
|
+
postId: posts[0].id,
|
|
92
|
+
viewedAt: (0, moment_1.default)().valueOf(),
|
|
93
|
+
}),
|
|
94
|
+
store.userPosts.create({
|
|
95
|
+
id: `${users[1].id}:${posts[1].id}`,
|
|
96
|
+
userId: users[1].id,
|
|
97
|
+
postId: posts[1].id,
|
|
98
|
+
viewedAt: (0, moment_1.default)().valueOf(),
|
|
99
|
+
}),
|
|
100
|
+
store.userPosts.create({
|
|
101
|
+
id: `${users[2].id}:${posts[1].id}`,
|
|
102
|
+
userId: users[2].id,
|
|
103
|
+
postId: posts[1].id,
|
|
104
|
+
viewedAt: (0, moment_1.default)().valueOf(),
|
|
105
|
+
}),
|
|
106
|
+
]);
|
|
107
|
+
// a user likes a post
|
|
108
|
+
userPosts[2] = await store.userPosts.update({
|
|
109
|
+
...userPosts[2],
|
|
110
|
+
likedAt: (0, moment_1.default)().valueOf(),
|
|
111
|
+
});
|
|
112
|
+
// load all of a user's viewed posts
|
|
113
|
+
console.log("=== A user's viewed posts ===");
|
|
114
|
+
const userViewedPosts = await store.userPosts.find({ user: users[1].id });
|
|
115
|
+
userViewedPosts.forEach((userPost) => {
|
|
116
|
+
console.log(`[user ${userPost.userId}]: ${userPost.id}`);
|
|
117
|
+
});
|
|
118
|
+
// load a post's count of views and likes
|
|
119
|
+
console.log("=== A post's views and likes ===");
|
|
120
|
+
const postViews = await store.userPosts.find({ post: posts[1].id });
|
|
121
|
+
console.log(`[post ${posts[1].id}]: ${postViews.filter((userPost) => userPost.viewedAt).length} view(s) and ${postViews.filter((userPost) => userPost.likedAt).length} like(s)`);
|
|
122
|
+
// cleanup from this session
|
|
123
|
+
// await Promise.all([
|
|
124
|
+
// ...users.map((user: User) => store.users.delete(user.id, { hardDelete: true })),
|
|
125
|
+
// ...posts.map((post: Post) => store.posts.delete(post.id, { hardDelete: true })),
|
|
126
|
+
// ...userPosts.map((userPost: UserPost) => store.userPosts.delete(userPost.id, { hardDelete: true })),
|
|
127
|
+
// ]);
|
|
128
|
+
// cleanup all from previous session
|
|
129
|
+
const [allUsers, allPosts, allUserPosts] = await Promise.all([
|
|
130
|
+
store.users.ids({ scan: "*", count: 999 }),
|
|
131
|
+
store.posts.ids({ scan: "*", count: 999 }),
|
|
132
|
+
store.userPosts.ids({ scan: "*", count: 999 }),
|
|
133
|
+
]);
|
|
134
|
+
await Promise.all([
|
|
135
|
+
...Array.from(allUsers).map((id) => store.users.delete(id, { hardDelete: true })),
|
|
136
|
+
...Array.from(allPosts).map((id) => store.posts.delete(id, { hardDelete: true })),
|
|
137
|
+
...Array.from(allUserPosts).map((id) => store.userPosts.delete(id, { hardDelete: true })),
|
|
138
|
+
]);
|
|
139
|
+
})();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const index_1 = require("./index");
|
|
4
|
+
const KV_REST_API_URL = "https://close-ringtail-23695.upstash.io";
|
|
5
|
+
const KV_REST_API_TOKEN = "AVyPAAIjcDE4ZDhiZjAxNjU3NjI0NGU1YjIwMzAzNzkzMWRkMDU2ZHAxMA";
|
|
6
|
+
const debug = false;
|
|
7
|
+
const PostRecordOptions = {
|
|
8
|
+
lookups: {
|
|
9
|
+
user: "createdBy",
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
const store = {
|
|
13
|
+
users: new index_1.RedisStore({
|
|
14
|
+
url: KV_REST_API_URL,
|
|
15
|
+
token: KV_REST_API_TOKEN,
|
|
16
|
+
key: "testuser",
|
|
17
|
+
debug,
|
|
18
|
+
}),
|
|
19
|
+
posts: new index_1.RedisStore({
|
|
20
|
+
url: KV_REST_API_URL,
|
|
21
|
+
token: KV_REST_API_TOKEN,
|
|
22
|
+
key: "testpost",
|
|
23
|
+
// recordOptions: PostRecordOptions,
|
|
24
|
+
debug,
|
|
25
|
+
}),
|
|
26
|
+
};
|
|
27
|
+
(async function () {
|
|
28
|
+
// create users
|
|
29
|
+
const users = await Promise.all([
|
|
30
|
+
store.users.create({ name: "User One" }),
|
|
31
|
+
store.users.create({ name: "User Two" }),
|
|
32
|
+
]);
|
|
33
|
+
// JSON.SET user:<UUID> $ '{ "id": "<UUID>", "createdAt": <TIMEINMILLIS>, "name": "User One" }'
|
|
34
|
+
// ZADD users <TIMEINMILLIS> <UUID>
|
|
35
|
+
// ...
|
|
36
|
+
console.log("users", users);
|
|
37
|
+
// users [
|
|
38
|
+
// { id: '<UUID>', createdAt: <TIMEINMILLIS>, name: 'User One' },
|
|
39
|
+
// ...
|
|
40
|
+
// ]
|
|
41
|
+
// create posts
|
|
42
|
+
const posts = await Promise.all([
|
|
43
|
+
store.posts.create({
|
|
44
|
+
createdBy: users[0].id,
|
|
45
|
+
message: "A post by user one",
|
|
46
|
+
}),
|
|
47
|
+
store.posts.create({
|
|
48
|
+
createdBy: users[0].id,
|
|
49
|
+
message: "Another post by user one",
|
|
50
|
+
}),
|
|
51
|
+
store.posts.create({
|
|
52
|
+
createdBy: users[0].id,
|
|
53
|
+
message: "Yet another post by user one",
|
|
54
|
+
}),
|
|
55
|
+
store.posts.create({
|
|
56
|
+
createdBy: users[1].id,
|
|
57
|
+
message: "A post by user two",
|
|
58
|
+
}),
|
|
59
|
+
]);
|
|
60
|
+
// JSON.SET post:<UUID> $ '{ "id": "<UUID>", "createdAt": <TIMEINMILLIS>, "createdBy": "<USER_UUID>", "message": "Another post by user one" }'
|
|
61
|
+
// ZADD posts <TIMEINMILLIS> <UUID>
|
|
62
|
+
// ZADD posts:user:<USER_UUID> <TIMEINMILLIS> <POST_UUID>
|
|
63
|
+
// ...
|
|
64
|
+
console.log("posts", posts);
|
|
65
|
+
// posts [
|
|
66
|
+
// {
|
|
67
|
+
// id: '<UUID>',
|
|
68
|
+
// createdAt: <TIMEINMILLIS>,
|
|
69
|
+
// createdBy: '<USER_UUID>',
|
|
70
|
+
// message: 'A post by user one'
|
|
71
|
+
// },
|
|
72
|
+
// ...
|
|
73
|
+
// ]
|
|
74
|
+
// all posts
|
|
75
|
+
const allPosts = await store.posts.find();
|
|
76
|
+
// ZRANGE posts:user:<USER_UUID> 0 -1 REV
|
|
77
|
+
// JSON.MGET post:<POST1_UUID> post:<POST2_UUID> ...
|
|
78
|
+
// latest posts from first user
|
|
79
|
+
const latestUserPosts = await store.posts.find({ user: users[0].id, count: 3 });
|
|
80
|
+
// ZRANGE posts:user:<USER_UUID> 0 2 REV
|
|
81
|
+
// JSON.MGET post:<POST1_UUID> post:<POST2_UUID> post:<POST3_UUID>
|
|
82
|
+
// cleanup from this session (soft delete by default)
|
|
83
|
+
await Promise.all([
|
|
84
|
+
...users.map((user) => store.users.delete(user.id)),
|
|
85
|
+
...posts.map((post) => store.posts.delete(post.id)),
|
|
86
|
+
]);
|
|
87
|
+
// JSON.SET user:<UUID> $.deletedAt <TIMEINMILLIS>
|
|
88
|
+
// ZREM users <UUID>
|
|
89
|
+
// ...
|
|
90
|
+
// JSON.SET post:<POST_UUID> $.deletedAt <TIMEINMILLIS>
|
|
91
|
+
// ZREM posts <POST_UUID>
|
|
92
|
+
// ZREM testposts:user:<USER_UUID> <POST_UUID>
|
|
93
|
+
// ...
|
|
94
|
+
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@desmat/redis-store",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"LICENSE"
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
|
-
"build": "tsc"
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"example": "ts-node --skipProject ./src/example.ts"
|
|
16
17
|
},
|
|
17
18
|
"repository": {
|
|
18
19
|
"type": "git",
|
|
@@ -28,9 +29,11 @@
|
|
|
28
29
|
"license": "MIT",
|
|
29
30
|
"dependencies": {
|
|
30
31
|
"@desmat/utils": "^1.0.0",
|
|
31
|
-
"@upstash/redis": "^1.34.3"
|
|
32
|
+
"@upstash/redis": "^1.34.3",
|
|
33
|
+
"dotenv": "^16.4.5"
|
|
32
34
|
},
|
|
33
35
|
"devDependencies": {
|
|
36
|
+
"@types/node": "^22.8.4",
|
|
34
37
|
"typescript": "^4.0.0"
|
|
35
38
|
}
|
|
36
|
-
}
|
|
39
|
+
}
|