model-redis 0.2.1 โ†’ 0.4.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.
Files changed (4) hide show
  1. package/README.md +291 -123
  2. package/index.js +8 -4
  3. package/package.json +19 -3
  4. package/src/redis_model.js +330 -230
package/README.md CHANGED
@@ -1,34 +1,67 @@
1
1
  # Model Redis
2
2
 
3
- Simple ORM model for Redis in NodsJS. The only external dependence is `redis`.
4
- This provides a simple ORM interface, with schema, for Redis. This is not meant
3
+ Simple ORM model for Redis in Node.js. The only external dependency is `redis`.
4
+ This provides a simple ORM interface, with schema, for Redis. This is not meant
5
5
  for large data sets and is geared more for small, internal infrastructure based
6
- projects that do not require complex data model.
6
+ projects that do not require complex data models.
7
7
 
8
+ ## Features
8
9
 
9
- ## Getting started
10
+ - ๐Ÿ“ฆ **CommonJS & ESM compatible** - Works seamlessly with both module systems
11
+ - ๐Ÿ“‹ Schema-based validation with type checking
12
+ - ๐Ÿ”‘ Primary key and indexed field support
13
+ - ๐Ÿ”— Model relationships (one-to-one, one-to-many)
14
+ - ๐Ÿ”„ Automatic type conversion (Redis strings โ†” native types)
15
+ - ๐Ÿ›ก๏ธ Field privacy control (exclude sensitive data from JSON)
16
+ - ๐Ÿงช Fully tested with 84%+ code coverage
17
+ - ๐Ÿท๏ธ Key prefixing support
10
18
 
11
- `setUpTable([object])` -- *Function* to bind the Redis connection
12
- object to the ORM table. It takes an optional connected redis client object
13
- or configuration for the Redis module. This will return a `Table` class we
14
- can use later for our model.
19
+ ## Installation
15
20
 
16
- It is recommend you place this in a utility or lib file with in your project
21
+ ```bash
22
+ npm install model-redis
23
+ ```
24
+
25
+ ## Getting Started
26
+
27
+ `setUpTable([object])` - Function to bind the Redis connection
28
+ to the ORM table. It takes an optional connected redis client object
29
+ or configuration for the Redis module. This will return a `Table` class we
30
+ can use later for our models.
31
+
32
+ The function returns synchronously, making it compatible with both CommonJS and ESM.
33
+ Redis connection happens in the background, and operations automatically await the connection.
34
+
35
+ It is recommended you place this in a utility or lib file within your project
17
36
  and require it when needed.
18
37
 
19
- The simplest way to use this is to pass nothing to the `setUpTable` function.
20
- this will create a connected client to Redis using the default settings:
38
+ ### CommonJS Usage
39
+
40
+ The simplest way to use this in CommonJS is to pass nothing to the `setUpTable` function.
41
+ This will create a connected client to Redis using the default settings:
21
42
 
22
43
  ```javascript
23
44
  'use strict';
24
45
 
25
- const {setUpTable} = require('model-redis')
46
+ const {setUpTable} = require('model-redis');
26
47
 
27
48
  const Table = setUpTable();
28
49
 
29
50
  module.exports = Table;
30
51
  ```
31
52
 
53
+ ### ESM Usage
54
+
55
+ For ESM projects, you can still use `await` if preferred (though it's no longer required):
56
+
57
+ ```javascript
58
+ import {setUpTable} from 'model-redis';
59
+
60
+ const Table = await setUpTable();
61
+
62
+ export default Table;
63
+ ```
64
+
32
65
  You can also pass your own configuration options to the Redis client. See the
33
66
  redis [client configuration guide](https://github.com/redis/node-redis/blob/master/docs/client-configuration.md)
34
67
  for available options:
@@ -39,12 +72,12 @@ for available options:
39
72
  const {setUpTable} = require('model-redis');
40
73
 
41
74
  const conf = {
42
- socket: {
43
- host: '10.10.10.10'
44
- port: 7676
45
- },
46
- username: admin,
47
- password: hunter42
75
+ socket: {
76
+ host: '10.10.10.10',
77
+ port: 7676
78
+ },
79
+ username: 'admin',
80
+ password: 'hunter42'
48
81
  };
49
82
 
50
83
  const Table = setUpTable({redisConf: conf});
@@ -53,27 +86,28 @@ module.exports = Table;
53
86
  ```
54
87
 
55
88
  It can also take a Redis client object, if you would like to have more control
56
- or use a custom version on Redis.
89
+ or use a custom version of Redis:
57
90
 
58
91
  ```javascript
59
92
  'use strict';
60
93
 
61
94
  const {setUpTable} = require('model-redis');
62
-
63
95
  const {createClient} = require('redis');
96
+
64
97
  const client = createClient();
65
- client.connect();
98
+ await client.connect();
66
99
 
67
100
  const Table = setUpTable({redisClient: client});
68
101
 
69
102
  module.exports = Table;
70
-
71
103
  ```
72
104
 
73
- ### Prefix key
105
+ **Note:** When passing a custom client, ensure it's connected before passing it to `setUpTable`.
106
+
107
+ ### Prefix Key
74
108
 
75
109
  At some point, the Redis package removed the option to prefix a string to the
76
- keys. This functionally has been added back with this package
110
+ keys. This functionality has been added back with this package:
77
111
 
78
112
  ```javascript
79
113
  'use strict';
@@ -81,148 +115,282 @@ keys. This functionally has been added back with this package
81
115
  const {setUpTable} = require('model-redis');
82
116
 
83
117
  const Table = setUpTable({
84
- prefix: 'auth_app'
118
+ prefix: 'auth_app:'
85
119
  });
86
120
 
87
121
  module.exports = Table;
88
122
  ```
89
123
 
90
- Once we have have our table object, we can start building using the ORM!
124
+ Once we have our table object, we can start building using the ORM!
91
125
 
92
126
  ## ORM API
93
127
 
94
128
  The Table class implements static and bound functions to perform normal ORM
95
129
  operations. For the rest of these examples, we will implement a simple user
96
- backing. This will show some usage and extenabilty:
130
+ backend. This will show some usage and extensibility:
97
131
 
98
- ``` javascript
99
- const Table = require('../utils/redis_model'); // Path to where the 'model-redis module is loaded and configured'
100
- const {Token, InviteToken} = require('./token');
132
+ ```javascript
133
+ const Table = require('../utils/redis_model'); // Path to where the 'model-redis' module is loaded and configured
101
134
  const bcrypt = require('bcrypt'); // We will use this for passwords later
102
135
  const saltRounds = 10;
103
136
 
104
- class User extends Table{
105
- static _key = 'username';
106
- static _keyMap = {
107
- 'created_by': {isRequired: true, type: 'string', min: 3, max: 500},
108
- 'created_on': {default: function(){return (new Date).getTime()}},
109
- 'updated_by': {default:"__NONE__", type: 'string',},
110
- 'updated_on': {default: function(){return (new Date).getTime()}, always: true},
111
- 'username': {isRequired: true, type: 'string', min: 3, max: 500},
112
- 'password': {isRequired: true, type: 'string', min: 3, max: 500},
113
- };
114
-
115
- static async add(data) {
116
- try{
117
- data['password'] = await bcrypt.hash(data['password'], saltRounds);
118
-
119
- return await super.add(data);
120
- }catch(error){
121
- throw error;
122
- }
123
- }
124
-
125
- async setPassword(data){
126
- try{
127
- data['password'] = await bcrypt.hash(data['password'], saltRounds);
128
-
129
- return this.update(data);
130
- }catch(error){
131
- throw error;
132
- }
133
- }
134
-
135
- static async login(data){
136
- try{
137
- let user = await User.get(data);
138
- let auth = await bcrypt.compare(data.password, user.password);
139
-
140
- if(auth){
141
- return user;
142
- }else{
143
- throw new Error("LogginFailed");
144
- }
145
- }catch(error){
146
- throw new Error("LogginFailed")
147
- }
148
- };
137
+ class User extends Table {
138
+ static _key = 'username';
139
+ static _keyMap = {
140
+ 'created_by': {isRequired: true, type: 'string', min: 3, max: 500},
141
+ 'created_on': {default: function(){return Date.now()}, type: 'number'},
142
+ 'updated_by': {default: "__NONE__", type: 'string'},
143
+ 'updated_on': {default: function(){return Date.now()}, type: 'number', always: true},
144
+ 'username': {isRequired: true, type: 'string', min: 3, max: 500},
145
+ 'password': {isRequired: true, type: 'string', min: 3, max: 500, isPrivate: true},
146
+ 'email': {isRequired: true, type: 'string'}
147
+ };
148
+
149
+ static async create(data) {
150
+ try {
151
+ data['password'] = await bcrypt.hash(data['password'], saltRounds);
152
+ return await super.create(data);
153
+ } catch(error) {
154
+ throw error;
155
+ }
156
+ }
157
+
158
+ async setPassword(newPassword) {
159
+ try {
160
+ const hashedPassword = await bcrypt.hash(newPassword, saltRounds);
161
+ return this.update({password: hashedPassword});
162
+ } catch(error) {
163
+ throw error;
164
+ }
165
+ }
166
+
167
+ static async login(data) {
168
+ try {
169
+ let user = await User.get(data.username);
170
+ let auth = await bcrypt.compare(data.password, user.password);
171
+
172
+ if(auth) {
173
+ return user;
174
+ } else {
175
+ throw new Error("LoginFailed");
176
+ }
177
+ } catch(error) {
178
+ throw new Error("LoginFailed");
179
+ }
180
+ }
149
181
  }
150
182
 
151
183
  module.exports = {User};
152
-
153
184
  ```
154
185
 
155
- ### Table schema
186
+ ### Table Schema
156
187
 
157
- The table schema a required aspect of using this module. The schema is defined
158
- with `_key`, `_indexed` and `_keyMap`
188
+ The table schema is a required aspect of using this module. The schema is defined
189
+ with `_key` and `_keyMap`:
159
190
 
160
191
  * `static _key` *string* is required and is basically the primary key for this
161
- table. It MUST match one of the keys in the `_keyMap` schema
162
-
163
- * `static _indexed` *array* is optional list of keys to be indexed. Indexed keys
164
- can be searched by with the `list()` and `listDetial()` methods.
192
+ table. It MUST match one of the keys in the `_keyMap` schema
165
193
 
166
194
  * `static _keyMap` *object* is required and defines the allowed schema for the
167
- table. Validation will be enforced based on what is defined in the schema.
195
+ table. Validation will be enforced based on what is defined in the schema.
168
196
 
169
197
  The `_keyMap` schema is an object where the key is the name of the field and the
170
198
  value is an object with the options for that field:
199
+
171
200
  ```javascript
172
201
  'username': {isRequired: true, type: 'string', min: 3, max: 500}
173
-
174
202
  ```
175
203
 
176
- #### Field options:
177
-
178
- * `type` *string* Required The native type this field will be checked for, valid
179
- types are:
180
-
181
- * `string`
182
- * `number`
183
- * `boolean`
184
- * `object`
185
-
186
- * `isRequired` *boolean* If this is set to true, this must be set when a new
187
- entry is created. This has no effect on updates.
188
- * `default` *field type or function* if nothing is passed, this will be used be
189
- used. If a function is placed here, it will be called and its return value
190
- used.
191
- * `always` *boolean* If this is set, the `default` is set, then its value will
192
- always be used when calling update. This is useful for setting an updated on
193
- field or access count.
194
- * `min` *number* Used with *string* or *number* type to define the lower limit
195
- * `max` *number* Used with *string* or *number* type to define the max limit
204
+ #### Field Options:
205
+
206
+ * `type` *string* - The native type this field will be checked for. Valid types are:
207
+ * `string`
208
+ * `number`
209
+ * `boolean`
210
+ * `object`
211
+
212
+ * `isRequired` *boolean* - If set to true, this must be set when a new
213
+ entry is created. This has no effect on updates.
214
+
215
+ * `default` *value or function* - If nothing is passed, this will be used.
216
+ If a function is placed here, it will be called and its return value used.
217
+
218
+ * `always` *boolean* - If this is set and `default` is set, then its value will
219
+ always be used when calling update. This is useful for setting an "updated_on"
220
+ field or access count.
221
+
222
+ * `min` *number* - Used with *string* or *number* type to define the lower limit
223
+
224
+ * `max` *number* - Used with *string* or *number* type to define the max limit
225
+
226
+ * `isPrivate` *boolean* - If set to true, this field will be excluded from `toJSON()` output.
227
+ Useful for passwords or sensitive data.
228
+
229
+ * `model` *string* - For relationships, specify the model name to link to
230
+
231
+ * `rel` *string* - Relationship type: `'one'` or `'many'`
232
+
233
+ * `localKey` *string* - For relationships, the local field to use (defaults to `_key`)
234
+
235
+ * `remoteKey` *string* - For relationships, the remote field to match against
196
236
 
197
237
  Once we have defined a `_keyMap` schema, the table can be used.
198
238
 
199
- #### Methods
239
+ ## Methods
240
+
241
+ ### Static Methods
200
242
 
201
243
  Static methods are used to query data and create new entries.
202
244
 
203
- * `await add(data)` Creates and returns a new entry. The passed data object
204
- will be validated and a validation error(complete will all the key errors)
205
- will be thrown if validation fails. Any key passed in the data object that
206
- is not in the `_keyMap` schema will be dropped.
245
+ * `await create(data)` - Creates and returns a new entry. The passed data object
246
+ will be validated and a validation error (complete with all the key errors)
247
+ will be thrown if validation fails. Any key passed in the data object that
248
+ is not in the `_keyMap` schema will be dropped.
249
+
250
+ * `await list()` - Returns a list of the primary keys in the table.
251
+
252
+ * `await listDetail([options], [queryHelper])` - Returns a list of Table instances.
253
+ Can optionally filter by passing an options object: `{age: 30, active: true}`
207
254
 
208
- * `await list([index_field, [index value]])` Returns a list of the primary keys in
209
- the table. If you pass `index_field` and `index_value`, only those matching
210
- will be returned.
255
+ * `await findall([options])` - Alias for `listDetail()`
211
256
 
212
- * `await listDetial([index_field, [index value]])` same as `list`, but will
213
- return a list of Table instances.
257
+ * `await get(pk, [queryHelper])` - Returns a Table instance for the passed primary key.
258
+ If none is found, a not found error is thrown.
214
259
 
215
- * `await get(pk)` returns a Table instance for the passed object. If none is,
216
- found a not found error is thrown
260
+ * `await exists(pk)` - Returns `true` or `false` if the passed PK exists.
217
261
 
218
- * `await exists(pk)` Returns `true` or `false` if the passed PK exists.
262
+ * `register([Model])` - Registers a model in the global registry for relationships.
263
+
264
+ ### Instance Methods
219
265
 
220
266
  Instances of a Table have the following methods:
221
267
 
222
- * `await update(data)` updates the current instance with the newly passed data
223
- and returns a new instance with the updated data. Data validation is also.
268
+ * `await update(data)` - Updates the current instance with the newly passed data
269
+ and returns the updated instance. Data validation is also enforced.
270
+
271
+ * `await remove()` - Deletes the current Table instance and returns itself.
272
+
273
+ * `toJSON()` - Returns a plain JavaScript object representation of the instance.
274
+ Fields marked with `isPrivate: true` are excluded.
275
+
276
+ * `toString()` - Returns the primary key value as a string.
277
+
278
+ All of these methods are extensible so proper business logic can be implemented.
279
+
280
+ ## Relationships
281
+
282
+ Model Redis supports relationships between models through the model registry system:
283
+
284
+ ```javascript
285
+ const Table = setUpTable();
286
+
287
+ // Define User model
288
+ class User extends Table {
289
+ static _key = 'id';
290
+ static _keyMap = {
291
+ id: {type: 'string', isRequired: true},
292
+ name: {type: 'string', isRequired: true},
293
+ posts: {model: 'Post', rel: 'many', remoteKey: 'userId', localKey: 'id'}
294
+ };
295
+ }
296
+
297
+ // Define Post model
298
+ class Post extends Table {
299
+ static _key = 'id';
300
+ static _keyMap = {
301
+ id: {type: 'string', isRequired: true},
302
+ title: {type: 'string', isRequired: true},
303
+ userId: {type: 'string', isRequired: true},
304
+ user: {model: 'User', rel: 'one', localKey: 'userId'}
305
+ };
306
+ }
307
+
308
+ // Register models
309
+ User.register();
310
+ Post.register();
311
+
312
+ // Now relationships will be loaded automatically
313
+ const user = await User.get('user1');
314
+ console.log(user.posts); // Array of Post instances
315
+
316
+ const post = await Post.get('post1');
317
+ console.log(post.user); // User instance
318
+ ```
319
+
320
+ ### Cycle Detection
321
+
322
+ The QueryHelper class automatically prevents infinite loops in circular relationships:
323
+
324
+ ```javascript
325
+ // User has many Posts, Post belongs to User
326
+ // When loading a User, it loads Posts
327
+ // Each Post tries to load its User (circular)
328
+ // QueryHelper detects this and prevents infinite recursion
329
+ const user = await User.get('user1');
330
+ // user.posts will be loaded, but user.posts[0].user won't recurse
331
+ ```
332
+
333
+ ## Error Handling
334
+
335
+ The module provides custom error types:
336
+
337
+ * `ObjectValidateError` - Thrown when validation fails, includes array of field errors
338
+ * `EntryNotFound` - Thrown when trying to get a non-existent entry
339
+ * `EntryNameUsed` - Thrown when trying to create an entry with an existing primary key
340
+
341
+ ```javascript
342
+ try {
343
+ await User.create({username: 'john'}); // Missing required 'email'
344
+ } catch(error) {
345
+ if(error.name === 'ObjectValidateError') {
346
+ console.log(error.message); // Array of validation errors
347
+ console.log(error.status); // 422
348
+ }
349
+ }
350
+ ```
351
+
352
+ ## Testing
353
+
354
+ The project includes a comprehensive test suite:
355
+
356
+ ```bash
357
+ # Run all tests
358
+ npm test
359
+
360
+ # Run tests in watch mode
361
+ npm run test:watch
362
+
363
+ # Run tests with coverage
364
+ npm run test:coverage
365
+ ```
366
+
367
+ ### Test Coverage
368
+
369
+ - **84.88%** overall coverage
370
+ - **67 tests** (66 passing, 1 skipped)
371
+ - Tests for validation, CRUD operations, filtering, and serialization
372
+
373
+ ## Development
374
+
375
+ ```bash
376
+ # Install dependencies
377
+ npm install
378
+
379
+ # Run tests
380
+ npm test
381
+
382
+ # Generate coverage report
383
+ npm run test:coverage
384
+ ```
385
+
386
+ ## License
387
+
388
+ MIT
389
+
390
+ ## Contributing
391
+
392
+ Issues and pull requests are welcome! Please see the [issues page](https://github.com/wmantly/model-redis/issues) for current bugs and feature requests.
224
393
 
225
- * `await remove()` Deletes the current Table instance and returns the delete
226
- count, this should be 1.
394
+ ## Known Issues
227
395
 
228
- All of these methods are extendable so proper business logic can be implemented.
396
+ - [Issue #3](https://github.com/wmantly/model-redis/issues/3) - Memory leak in relationship test suite (does not affect production usage)
package/index.js CHANGED
@@ -5,17 +5,21 @@ var client = null
5
5
  function setUpTable(obj){
6
6
  obj = obj || {};
7
7
 
8
+ let connectionPromise;
9
+
8
10
  if(obj.redisClient){
9
11
  client = obj.redisClient;
12
+ // If a client is provided, assume it's already connected or will be connected externally
13
+ connectionPromise = Promise.resolve(client);
10
14
  }else{
11
15
  const {createClient} = require('redis');
12
16
  client = createClient(obj.redisConf || {});
13
- client.connect();
17
+ // Connect in background and store the promise
18
+ connectionPromise = client.connect().then(() => client);
14
19
  }
15
20
 
16
- // test client connection
17
-
18
- return table(client, obj.prefix);
21
+ // Return Table class immediately with connection promise injected
22
+ return table(client, obj.prefix, connectionPromise);
19
23
  }
20
24
 
21
25
  module.exports = {client, setUpTable};
package/package.json CHANGED
@@ -1,10 +1,23 @@
1
1
  {
2
2
  "name": "model-redis",
3
- "version": "0.2.1",
4
- "description": "Simple ORM model for redis in NodsJS",
3
+ "version": "0.4.0",
4
+ "description": "Simple ORM model for Redis in Node.js",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
7
+ "test": "jest",
8
+ "test:watch": "jest --watch",
9
+ "test:coverage": "jest --coverage"
10
+ },
11
+ "jest": {
12
+ "testEnvironment": "node",
13
+ "coverageDirectory": "coverage",
14
+ "collectCoverageFrom": [
15
+ "src/**/*.js",
16
+ "index.js"
17
+ ],
18
+ "testMatch": [
19
+ "**/tests/**/*.test.js"
20
+ ]
8
21
  },
9
22
  "repository": {
10
23
  "type": "git",
@@ -23,5 +36,8 @@
23
36
  "homepage": "https://github.com/wmantly/model-redis#readme",
24
37
  "dependencies": {
25
38
  "redis": "^4.6.10"
39
+ },
40
+ "devDependencies": {
41
+ "jest": "^30.2.0"
26
42
  }
27
43
  }
@@ -2,237 +2,337 @@
2
2
 
3
3
  const objValidate = require('./object_validate');
4
4
 
5
+ class QueryHelper{
6
+ history = []
7
+ constructor(origin){
8
+ this.origin = origin
9
+ this.history.push(origin.constructor.name);
10
+ }
11
+
12
+ static isNotCycle(modelName, queryHelper){
13
+ if(!(queryHelper instanceof this)){
14
+ return true; // No queryHelper, can't detect cycles
15
+ }
16
+ if(queryHelper.history.includes(modelName)){
17
+ return false; // Cycle detected - return false to skip
18
+ }
19
+ queryHelper.history.push(modelName);
20
+ return true; // No cycle detected - return true to continue
21
+ }
22
+ }
5
23
 
6
-
7
- function setUpTable(client, prefix=''){
8
-
9
- function redisPrefix(key){
10
- return `${prefix}${key}`;
11
- }
12
-
13
- class Table{
14
- static _indexed = [];
15
-
16
- constructor(data){
17
- for(let key in data){
18
- this[key] = data[key];
19
- }
20
- }
21
-
22
- static async get(index){
23
- try{
24
-
25
- if(typeof index === 'object'){
26
- index = index[this._key]
27
- }
28
-
29
- console.log('get', redisPrefix(`${this.prototype.constructor.name}_${index}`))
30
-
31
- let result = await client.HGETALL(
32
- redisPrefix(`${this.prototype.constructor.name}_${index}`)
33
- );
34
-
35
- if(Object.keys(result).length === 0){
36
- let error = new Error('EntryNotFound');
37
- error.name = 'EntryNotFound';
38
- error.message = `${this.prototype.constructor.name}:${index} does not exists`;
39
- error.status = 404;
40
- throw error;
41
- }
42
-
43
- // Redis always returns strings, use the keyMap schema to turn them
44
- // back to native values.
45
- result = objValidate.parseFromString(this._keyMap, result);
46
-
47
- return new this.prototype.constructor(result)
48
-
49
- }catch(error){
50
- throw error;
51
- }
52
-
53
- }
54
-
55
- static async exists(data){
56
- try{
57
- await this.get(data);
58
-
59
- return true
60
- }catch(error){
61
- return false;
62
- }
63
- }
64
-
65
- static async list(index_key, value){
66
- // return a list of all the index keys for this table.
67
- try{
68
-
69
-
70
- console.log('here')
71
-
72
- if(index_key && !this._indexed.includes(index_key)) return [];
73
- console.log('here2', redisPrefix(`${this.prototype.constructor.name}_${index_key}_${value}`))
74
-
75
- if(index_key && this._indexed.includes(index_key)){
76
- return await client.SMEMBERS(
77
- redisPrefix(`${this.prototype.constructor.name}_${index_key}_${value}`)
78
- );
79
- }
80
- console.log('here3', redisPrefix(this.prototype.constructor.name))
81
-
82
- return await client.SMEMBERS(
83
- redisPrefix(this.prototype.constructor.name));
84
-
85
- }catch(error){
86
- throw error;
87
- }
88
- }
89
-
90
- static async listDetail(index_key, value){
91
- // Return a list of the entries as instances.
92
- let out = [];
93
-
94
- for(let entry of await this.list(index_key, value)){
95
- out.push(await this.get(entry));
96
- }
97
-
98
- return out;
99
- }
100
-
101
- static async add(data){
102
- // Add a entry to this redis table.
103
- try{
104
- // Validate the passed data by the keyMap schema.
105
-
106
- data = objValidate.processKeys(this._keyMap, data);
107
-
108
- // Do not allow the caller to overwrite an existing index key,
109
- if(data[this._key] && await this.exists(data)){
110
- let error = new Error('EntryNameUsed');
111
- error.name = 'EntryNameUsed';
112
- error.message = `${this.prototype.constructor.name}:${data[this._key]} already exists`;
113
- error.status = 409;
114
-
115
- throw error;
116
- }
117
-
118
- // Add the key to the members for this redis table
119
- await client.SADD(
120
- redisPrefix(this.prototype.constructor.name),
121
- String(data[this._key])
122
- );
123
-
124
- // Create index keys lists
125
- for(let index of this._indexed){
126
- if(data[index]) await client.SADD(
127
- redisPrefix(`${this.prototype.constructor.name}_${index}_${data[index]}`),
128
- String(data[this._key]
129
- ));
130
- }
131
-
132
- // Add the values for this entry.
133
- for(let key of Object.keys(data)){
134
- await client.HSET(
135
- redisPrefix(`${this.prototype.constructor.name}_${data[this._key]}`),
136
- key, objValidate.parseToString(data[key])
137
- );
138
- }
139
-
140
- // return the created redis entry as entry instance.
141
- return await this.get(data[this._key]);
142
- } catch(error){
143
- throw error;
144
- }
145
- }
146
-
147
- async update(data, key){
148
- // Update an existing entry.
149
- try{
150
- // Check to see if entry name changed.
151
- if(data[this.constructor._key] && data[this.constructor._key] !== this[this.constructor._key]){
152
-
153
- // Merge the current data into with the updated data
154
- let newData = Object.assign({}, this, data);
155
-
156
- // Remove the updated failed so it doesnt keep it
157
- delete newData.updated;
158
-
159
- // Create a new record for the updated entry. If that succeeds,
160
- // delete the old recored
161
- if(await this.add(newData)) await this.remove();
162
-
163
- }else{
164
- // Update what ever fields that where passed.
165
-
166
- // Validate the passed data, ignoring required fields.
167
- data = objValidate.processKeys(this.constructor._keyMap, data, true);
168
-
169
- // Update the index keys
170
- for(let index of this.constructor._indexed){
171
- if(data[index]){
172
- await client.SREM(
173
- redisPrefix(`${this.constructor.name}_${index}_${this[index]}`),
174
- String(this[this.constructor._key])
175
- );
176
-
177
- await client.SADD(
178
- redisPrefix(`${this.constructor.name}_${index}_${data[index]}`),
179
- String(data[this.constructor._key] || this[this.constructor._key])
180
- );
181
- }
182
-
183
- }
184
- // Loop over the data fields and apply them to redis
185
- for(let key of Object.keys(data)){
186
- this[key] = data[key];
187
- await client.HSET(
188
- redisPrefix(`${this.constructor.name}_${this[this.constructor._key]}`),
189
- key, data[key]
190
- );
191
- }
192
- }
193
-
194
- return this;
195
-
196
- } catch(error){
197
- // Pass any error to the calling function
198
- throw error;
199
- }
200
- }
201
-
202
- async remove(data){
203
- // Remove an entry from this table.
204
-
205
- try{
206
- // Remove the index key from the tables members list.
207
-
208
- await client.SREM(
209
- redisPrefix(this.constructor.name),
210
- this[this.constructor._key]
211
- );
212
-
213
- for(let index of this.constructor._indexed){
214
- await client.SREM(
215
- redisPrefix(`${this.constructor.name}_${index}_${data[value]}`),
216
- data[this.constructor._key]
217
- );
218
- }
219
-
220
- // Remove the entries hash values.
221
- let count = await client.DEL(
222
- redisPrefix(
223
- `${this.constructor.name}_${this[this.constructor._key]}`)
224
- );
225
-
226
- // Return the number of removed values to the caller.
227
- return count;
228
-
229
- } catch(error) {
230
- throw error;
231
- }
232
- };
233
- }
234
-
235
- return Table;
24
+ function setUpTable(client, prefix='', connectionPromise=null){
25
+
26
+ function redisPrefix(key){
27
+ return `${prefix}${key}`;
28
+ }
29
+
30
+ // Helper function to await connection if promise exists
31
+ async function ensureClientReady(){
32
+ if(connectionPromise){
33
+ await connectionPromise;
34
+ }
35
+ }
36
+
37
+ class Table{
38
+ static errors = {
39
+ ObjectValidateError: objValidate.ObjectValidateError,
40
+ EntryNameUsed: ()=>{
41
+ let error = new Error('EntryNameUsed');
42
+ error.name = 'EntryNameUsed';
43
+ error.message = `${this.prototype.constructor.name}:${data[this._key]} already exists`;
44
+ error.keys = [{
45
+ key: this._key,
46
+ message: `${this.prototype.constructor.name}:${data[this._key]} already exists`
47
+ }]
48
+ error.status = 409;
49
+
50
+ return error;
51
+ }
52
+ }
53
+
54
+ static redisClient = client;
55
+
56
+ static models = {}
57
+ static register = function(Model){
58
+ Model = Model || this;
59
+ this.models[Model.name] = Model;
60
+ }
61
+
62
+ constructor(data){
63
+ for(let key in data){
64
+ this[key] = data[key];
65
+ }
66
+ }
67
+
68
+ static async get(index, queryHelper){
69
+ try{
70
+ // Ensure client is connected before proceeding
71
+ await ensureClientReady();
72
+
73
+ if(typeof index === 'object'){
74
+ index = index[this._key];
75
+ }
76
+
77
+ let result = await client.HGETALL(
78
+ redisPrefix(`${this.prototype.constructor.name}_${index}`)
79
+ );
80
+
81
+ if(!result || !Object.keys(result).length){
82
+ let error = new Error('EntryNotFound');
83
+ error.name = 'EntryNotFound';
84
+ error.message = `${this.prototype.constructor.name}:${index} does not exists`;
85
+ error.status = 404;
86
+ throw error;
87
+ }
88
+
89
+ // Redis always returns strings, use the keyMap schema to turn them
90
+ // back to native values.
91
+ result = objValidate.parseFromString(this._keyMap, result);
92
+
93
+ let instance = new this(result);
94
+ await instance.buildRelations(queryHelper);
95
+
96
+ return instance;
97
+ }catch(error){
98
+ throw error;
99
+ }
100
+ }
101
+
102
+ async buildRelations(queryHelper){
103
+ // Create QueryHelper if not provided
104
+ if(!queryHelper){
105
+ queryHelper = new QueryHelper(this);
106
+ }
107
+
108
+ for(let [key, options] of Object.entries(this.constructor._keyMap)){
109
+ if(options.model){
110
+ let remoteModel = this.constructor.models[options.model]
111
+ try{
112
+ if(!QueryHelper.isNotCycle(remoteModel.name, queryHelper)) continue;
113
+ if(options.rel === 'one'){
114
+ this[key] = await remoteModel.get(this[key] || this[options.localKey || this.constructor._key] , queryHelper)
115
+ }
116
+ if(options.rel === 'many'){
117
+ this[key] = await remoteModel.listDetail({
118
+ [options.remoteKey]: this[options.localKey || this.constructor._key],
119
+ }, queryHelper)
120
+
121
+ }
122
+ }catch(error){
123
+ // Silently ignore relation loading errors (record may not exist)
124
+ }
125
+ }
126
+ }
127
+ }
128
+
129
+ static async exists(index){
130
+ // Ensure client is connected before proceeding
131
+ await ensureClientReady();
132
+
133
+ if(typeof index === 'object'){
134
+ index = index[this._key];
135
+ }
136
+
137
+ return Boolean(await client.SISMEMBER(
138
+ redisPrefix(this.prototype.constructor.name),
139
+ index
140
+ ));
141
+ }
142
+
143
+ static async list(){
144
+ // return a list of all the index keys for this table.
145
+ try{
146
+ // Ensure client is connected before proceeding
147
+ await ensureClientReady();
148
+
149
+ return await client.SMEMBERS(
150
+ redisPrefix(this.prototype.constructor.name)
151
+ );
152
+
153
+ }catch(error){
154
+ throw error;
155
+ }
156
+ }
157
+
158
+ static async listDetail(options, queryHelper){
159
+
160
+ // Return a list of the entries as instances.
161
+ let out = [];
162
+
163
+ for(let entry of await this.list()){
164
+ let instance = await this.get(entry, arguments[arguments.length - 1]);
165
+ if(!options) out.push(instance);
166
+ let matchCount = 0;
167
+ for(let option in options){
168
+ if(instance[option] === options[option] && ++matchCount === Object.keys(options).length){
169
+ out.push(instance);
170
+ break;
171
+ }
172
+ }
173
+ }
174
+
175
+ return out;
176
+ }
177
+
178
+ static findall(...args){
179
+ return this.listDetail(...args);
180
+ }
181
+
182
+ static async create(data){
183
+ // Add a entry to this redis table.
184
+ try{
185
+ // Ensure client is connected before proceeding
186
+ await ensureClientReady();
187
+
188
+ // Validate the passed data by the keyMap schema.
189
+ data = objValidate.processKeys(this._keyMap, data);
190
+
191
+ // Do not allow the caller to overwrite an existing index key,
192
+ if(data[this._key] && await this.exists(data)){
193
+ let error = new Error('EntryNameUsed');
194
+ error.name = 'EntryNameUsed';
195
+ error.message = `${this.prototype.constructor.name}:${data[this._key]} already exists`;
196
+ error.keys = [{
197
+ key: this._key,
198
+ message: `${this.prototype.constructor.name}:${data[this._key]} already exists`
199
+ }]
200
+ error.status = 409;
201
+
202
+ throw error;
203
+ }
204
+
205
+ // Add the key to the members for this redis table
206
+ await client.SADD(
207
+ redisPrefix(this.prototype.constructor.name),
208
+ data[this._key]
209
+ );
210
+
211
+ // Add the values for this entry.
212
+ for(let key of Object.keys(data)){
213
+ if(data[key] === undefined) continue;
214
+ await client.HSET(
215
+ redisPrefix(`${this.prototype.constructor.name}_${data[this._key]}`),
216
+ key,
217
+ objValidate.parseToString(data[key])
218
+ );
219
+ }
220
+
221
+ // return the created redis entry as entry instance.
222
+ return await this.get(data[this._key]);
223
+ } catch(error){
224
+ throw error;
225
+ }
226
+ }
227
+
228
+ async update(data){
229
+ // Update an existing entry.
230
+ try{
231
+ // Ensure client is connected before proceeding
232
+ await ensureClientReady();
233
+
234
+ // Validate the passed data, ignoring required fields.
235
+ data = objValidate.processKeys(this.constructor._keyMap, data, true);
236
+
237
+ // Check to see if entry name changed.
238
+ if(data[this.constructor._key] && data[this.constructor._key] !== this[this.constructor._key]){
239
+ // Remove the index key from the tables members list.
240
+
241
+ if(data[this.constructor._key] && await this.constructor.exists(data)){
242
+ let error = new Error('EntryNameUsed');
243
+ error.name = 'EntryNameUsed';
244
+ error.message = `${this.constructor.name}:${data[this.constructor._key]} already exists`;
245
+ error.keys = [{
246
+ key: this.constructor._key,
247
+ message: `${this.constructor.name}:${data[this.constructor._key]} already exists`
248
+ }]
249
+ error.status = 409;
250
+
251
+ throw error;
252
+ }
253
+
254
+ await client.SREM(
255
+ redisPrefix(this.constructor.name),
256
+ this[this.constructor._key]
257
+ );
258
+
259
+ // Add the key to the members for this redis table
260
+ await client.SADD(
261
+ redisPrefix(this.constructor.name),
262
+ data[this.constructor._key]
263
+ );
264
+
265
+ await client.RENAME(
266
+ redisPrefix(`${this.constructor.name}_${this[this.constructor._key]}`),
267
+ redisPrefix(`${this.constructor.name}_${data[this.constructor._key]}`),
268
+ );
269
+
270
+ }
271
+ // Update what ever fields that where passed.
272
+
273
+ // Loop over the data fields and apply them to redis
274
+ for(let key of Object.keys(data)){
275
+ this[key] = data[key];
276
+ await client.HSET(
277
+ redisPrefix(`${this.constructor.name}_${this[this.constructor._key]}`),
278
+ key, objValidate.parseToString(data[key])
279
+ );
280
+ }
281
+
282
+
283
+ return this;
284
+
285
+ } catch(error){
286
+ // Pass any error to the calling function
287
+ throw error;
288
+ }
289
+ }
290
+
291
+ async remove(data){
292
+ // Remove an entry from this table.
293
+
294
+ try{
295
+ // Ensure client is connected before proceeding
296
+ await ensureClientReady();
297
+
298
+ // Remove the index key from the tables members list.
299
+ await client.SREM(
300
+ redisPrefix(this.constructor.name),
301
+ this[this.constructor._key]
302
+ );
303
+
304
+ // Remove the entries hash values.
305
+ let count = await client.DEL(
306
+ redisPrefix(`${this.constructor.name}_${this[this.constructor._key]}`)
307
+ );
308
+
309
+ // Return the number of removed values to the caller.
310
+ return this;
311
+
312
+ } catch(error) {
313
+ throw error;
314
+ }
315
+ };
316
+
317
+ toJSON(){
318
+ let result = {};
319
+ for (const [key, value] of Object.entries(this)) {
320
+ if(this.constructor._keyMap[key] && this.constructor._keyMap[key].isPrivate) continue;
321
+ result[key] = value;
322
+ }
323
+
324
+ return result
325
+
326
+ // return JSON.stringify(result);
327
+ }
328
+
329
+ toString(){
330
+ return this[this.constructor._key];
331
+ }
332
+
333
+ }
334
+
335
+ return Table;
236
336
  }
237
337
 
238
338
  module.exports = setUpTable;