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