model-redis 0.2.1 โ†’ 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 +274 -126
  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,30 +1,45 @@
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
  ```
@@ -39,190 +54,323 @@ for available options:
39
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
57
+ socket: {
58
+ host: '10.10.10.10',
59
+ port: 7676
60
+ },
61
+ username: 'admin',
62
+ password: 'hunter42'
48
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
76
  const {setUpTable} = require('model-redis');
62
-
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;
70
-
71
85
  ```
72
86
 
73
- ### Prefix key
87
+ ### Prefix Key
74
88
 
75
89
  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
90
+ keys. This functionality has been added back with this package:
77
91
 
78
92
  ```javascript
79
93
  'use strict';
80
94
 
81
95
  const {setUpTable} = require('model-redis');
82
96
 
83
- const Table = setUpTable({
84
- prefix: 'auth_app'
97
+ const Table = await setUpTable({
98
+ prefix: 'auth_app:'
85
99
  });
86
100
 
87
101
  module.exports = Table;
88
102
  ```
89
103
 
90
- 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!
91
105
 
92
106
  ## ORM API
93
107
 
94
108
  The Table class implements static and bound functions to perform normal ORM
95
109
  operations. For the rest of these examples, we will implement a simple user
96
- backing. This will show some usage and extenabilty:
110
+ backend. This will show some usage and extensibility:
97
111
 
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');
112
+ ```javascript
113
+ const Table = require('../utils/redis_model'); // Path to where the 'model-redis' module is loaded and configured
101
114
  const bcrypt = require('bcrypt'); // We will use this for passwords later
102
115
  const saltRounds = 10;
103
116
 
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
- };
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
+ }
149
161
  }
150
162
 
151
163
  module.exports = {User};
152
-
153
164
  ```
154
165
 
155
- ### Table schema
166
+ ### Table Schema
156
167
 
157
- The table schema a required aspect of using this module. The schema is defined
158
- 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`:
159
170
 
160
171
  * `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.
172
+ table. It MUST match one of the keys in the `_keyMap` schema
165
173
 
166
174
  * `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.
175
+ table. Validation will be enforced based on what is defined in the schema.
168
176
 
169
177
  The `_keyMap` schema is an object where the key is the name of the field and the
170
178
  value is an object with the options for that field:
179
+
171
180
  ```javascript
172
181
  'username': {isRequired: true, type: 'string', min: 3, max: 500}
173
-
174
182
  ```
175
183
 
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
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
196
216
 
197
217
  Once we have defined a `_keyMap` schema, the table can be used.
198
218
 
199
- #### Methods
219
+ ## Methods
220
+
221
+ ### Static Methods
200
222
 
201
223
  Static methods are used to query data and create new entries.
202
224
 
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.
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}`
207
234
 
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.
235
+ * `await findall([options])` - Alias for `listDetail()`
211
236
 
212
- * `await listDetial([index_field, [index value]])` same as `list`, but will
213
- 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.
214
239
 
215
- * `await get(pk)` returns a Table instance for the passed object. If none is,
216
- found a not found error is thrown
240
+ * `await exists(pk)` - Returns `true` or `false` if the passed PK exists.
217
241
 
218
- * `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
219
245
 
220
246
  Instances of a Table have the following methods:
221
247
 
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.
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.
224
373
 
225
- * `await remove()` Deletes the current Table instance and returns the delete
226
- count, this should be 1.
374
+ ## Known Issues
227
375
 
228
- 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.1",
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;