From 5913c5e8c3d9fe6a445bc5f24afd7a822ca4005e Mon Sep 17 00:00:00 2001 From: William Mantly Date: Wed, 31 Dec 2025 20:32:21 -0500 Subject: [PATCH 1/2] Moved to model-redis package --- nodejs/bin/www | 2 +- nodejs/models/index.js | 2 +- nodejs/package-lock.json | 101 ++++++++++++ nodejs/package.json | 1 + nodejs/utils/redis_model.js | 308 +----------------------------------- 5 files changed, 109 insertions(+), 305 deletions(-) diff --git a/nodejs/bin/www b/nodejs/bin/www index 2e91d94b..c94e4220 100755 --- a/nodejs/bin/www +++ b/nodejs/bin/www @@ -95,4 +95,4 @@ function onListening() { for(let listener of app.onListen){ listener() } -} \ No newline at end of file +} diff --git a/nodejs/models/index.js b/nodejs/models/index.js index 36b62cc8..c3305890 100644 --- a/nodejs/models/index.js +++ b/nodejs/models/index.js @@ -1,6 +1,6 @@ 'use strict'; -const Table = require('../utils/redis_model') +const Table = require('../utils/redis_model'); module.exports = Table; require('./dns_provider'); diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index 54c90c8e..5d8fba6a 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -23,6 +23,7 @@ "jquery": "^3.7.1", "ldapts": "^8.1.2", "linux-sys-user": "^1.2.0", + "model-redis": "^0.4.0", "moment": "^2.30.1", "mustache": "^4.2.0", "p2psub": "^0.2.0", @@ -1013,6 +1014,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generic-pool": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", + "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -1352,6 +1362,91 @@ "node": ">=10" } }, + "node_modules/model-redis": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/model-redis/-/model-redis-0.4.0.tgz", + "integrity": "sha512-E6WEhjrKhL8C+2cc9g6htpPqthwz89BREke+hIE/KAEROaUy/7xAi0dUf4BVsrUCKXzHEXlsyyjQZ72aS8bS/g==", + "license": "MIT", + "dependencies": { + "redis": "^4.6.10" + } + }, + "node_modules/model-redis/node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/model-redis/node_modules/@redis/client": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz", + "integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/model-redis/node_modules/@redis/graph": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", + "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/model-redis/node_modules/@redis/json": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", + "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/model-redis/node_modules/@redis/search": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", + "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/model-redis/node_modules/@redis/time-series": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", + "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", + "license": "MIT", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/model-redis/node_modules/redis": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.1.tgz", + "integrity": "sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==", + "license": "MIT", + "workspaces": [ + "./packages/*" + ], + "dependencies": { + "@redis/bloom": "1.2.0", + "@redis/client": "1.6.1", + "@redis/graph": "1.1.1", + "@redis/json": "1.0.7", + "@redis/search": "1.2.0", + "@redis/time-series": "1.1.0" + } + }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -2141,6 +2236,12 @@ "optional": true } } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" } } } diff --git a/nodejs/package.json b/nodejs/package.json index e920be6e..0a9046fe 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -34,6 +34,7 @@ "jquery": "^3.7.1", "ldapts": "^8.1.2", "linux-sys-user": "^1.2.0", + "model-redis": "^0.4.0", "moment": "^2.30.1", "mustache": "^4.2.0", "p2psub": "^0.2.0", diff --git a/nodejs/utils/redis_model.js b/nodejs/utils/redis_model.js index 03179f7f..e93724da 100644 --- a/nodejs/utils/redis_model.js +++ b/nodejs/utils/redis_model.js @@ -1,308 +1,10 @@ 'use strict'; -const {createClient} = require('redis'); -const objValidate = require('../utils/object_validate'); +const {setUpTable} = require('model-redis'); const conf = require('@simpleworkjs/conf'); -const client = createClient({}); -client.connect(); +const Table = setUpTable({ + prefix: conf.redis.prefix +}); -function redisPrefix(key){ - return `${conf.redis.prefix}${key}`; -} - -class QueryHelper{ - hisroty = [] - constructor(orgin){ - this.orgin = orgin - this.hisroty.push(orgin.constructor.name); - } - - static isNotCycle(modleName, queryHelper){ - if(queryHelper instanceof this){ - if(queryHelper.hisroty.includes(modleName)){ - return true; - } - queryHelper.hisroty.push(modleName) - } - } -} - -class Table{ - static errors = { - ObjectValidateError: objValidate.ObjectValidateError, - EntryNameUsed: ()=>{ - let error = new Error('EntryNameUsed'); - error.name = 'EntryNameUsed'; - error.message = `${this.prototype.constructor.name}:${data[this._key]} already exists`; - error.keys = [{ - key: this._key, - message: `${this.prototype.constructor.name}:${data[this._key]} already exists` - }] - error.status = 409; - - return error; - } - } - - static redisClient = client; - - static models = {} - static register = function(Model){ - Model = Model || this; - this.models[Model.name] = Model; - } - - constructor(data){ - for(let key in data){ - this[key] = data[key]; - } - } - - static async get(index, queryHelper){ - try{ - if(typeof index === 'object'){ - index = index[this._key]; - } - - let result = await client.HGETALL( - redisPrefix(`${this.prototype.constructor.name}_${index}`) - ); - - if(!Object.keys(result).length){ - let error = new Error('EntryNotFound'); - error.name = 'EntryNotFound'; - error.message = `${this.prototype.constructor.name}:${index} does not exists`; - error.status = 404; - throw error; - } - - // Redis always returns strings, use the keyMap schema to turn them - // back to native values. - result = objValidate.parseFromString(this._keyMap, result); - - let instance = new this(result); - await instance.buildRelations(queryHelper); - - return instance; - }catch(error){ - throw error; - } - } - - async buildRelations(queryHelper){ - - for(let [key, options] of Object.entries(this.constructor._keyMap)){ - if(options.model){ - let remoteModel = this.constructor.models[options.model] - try{ - if(QueryHelper.isNotCycle(remoteModel.name, queryHelper)) continue; - if(options.rel === 'one'){ - // console.log('relone:', this[key], queryHelper, remoteModel, await remoteModel.get(this[key], queryHelper || new QueryHelper(this))) - this[key] = await remoteModel.get(this[key] || this[options.localKey || this.constructor._key] , queryHelper || new QueryHelper(this)) - } - if(options.rel === 'many'){ - this[key] = await remoteModel.listDetail({ - [options.remoteKey]: this[options.localKey || this.constructor._key], - },queryHelper || new QueryHelper(this)) - - } - }catch{} - } - } - } - - static async exists(index){ - if(typeof index === 'object'){ - index = index[this._key]; - } - - return await client.SISMEMBER( - redisPrefix(this.prototype.constructor.name), - index - ); - } - - static async list(){ - // return a list of all the index keys for this table. - try{ - return await client.SMEMBERS( - redisPrefix(this.prototype.constructor.name) - ); - - }catch(error){ - throw error; - } - } - - static async listDetail(options, queryHelper){ - - // Return a list of the entries as instances. - let out = []; - - for(let entry of await this.list()){ - let instance = await this.get(entry, arguments[arguments.length - 1]); - if(!options) out.push(instance); - let matchCount = 0; - for(let option in options){ - if(instance[option] === options[option] && ++matchCount === Object.keys(options).length){ - out.push(instance); - break; - } - } - } - - return out; - } - - static findall(...args){ - return this.listDetail(...args); - } - - static async create(data){ - // Add a entry to this redis table. - try{ - - // Validate the passed data by the keyMap schema. - data = objValidate.processKeys(this._keyMap, data); - - // Do not allow the caller to overwrite an existing index key, - if(data[this._key] && await this.exists(data)){ - let error = new Error('EntryNameUsed'); - error.name = 'EntryNameUsed'; - error.message = `${this.prototype.constructor.name}:${data[this._key]} already exists`; - error.keys = [{ - key: this._key, - message: `${this.prototype.constructor.name}:${data[this._key]} already exists` - }] - error.status = 409; - - throw error; - } - - // Add the key to the members for this redis table - await client.SADD( - redisPrefix(this.prototype.constructor.name), - data[this._key] - ); - - // Add the values for this entry. - for(let key of Object.keys(data)){ - if(data[key] === undefined) continue; - await client.HSET( - redisPrefix(`${this.prototype.constructor.name}_${data[this._key]}`), - key, - objValidate.parseToString(data[key]) - ); - } - - // return the created redis entry as entry instance. - return await this.get(data[this._key]); - } catch(error){ - throw error; - } - } - - async update(data, key){ - // Update an existing entry. - try{ - // Validate the passed data, ignoring required fields. - data = objValidate.processKeys(this.constructor._keyMap, data, true); - - // Check to see if entry name changed. - if(data[this.constructor._key] && data[this.constructor._key] !== this[this.constructor._key]){ - // Remove the index key from the tables members list. - - if(data[this.constructor._key] && await this.constructor.exists(data)){ - let error = new Error('EntryNameUsed'); - error.name = 'EntryNameUsed'; - error.message = `${this.constructor.name}:${data[this.constructor._key]} already exists`; - error.keys = [{ - key: this.constructor._key, - message: `${this.constructor.name}:${data[this.constructor._key]} already exists` - }] - error.status = 409; - - throw error; - } - - await client.SREM( - redisPrefix(this.constructor.name), - this[this.constructor._key] - ); - - // Add the key to the members for this redis table - await client.SADD( - redisPrefix(this.constructor.name), - data[this.constructor._key] - ); - - await client.RENAME( - redisPrefix(`${this.constructor.name}_${this[this.constructor._key]}`), - redisPrefix(`${this.constructor.name}_${data[this.constructor._key]}`), - ); - - } - // Update what ever fields that where passed. - - // Loop over the data fields and apply them to redis - for(let key of Object.keys(data)){ - this[key] = data[key]; - await client.HSET( - redisPrefix(`${this.constructor.name}_${this[this.constructor._key]}`), - key, String(data[key]) - ); - } - - - return this; - - } catch(error){ - // Pass any error to the calling function - throw error; - } - } - - async remove(data){ - // Remove an entry from this table. - - try{ - // Remove the index key from the tables members list. - await client.SREM( - redisPrefix(this.constructor.name), - this[this.constructor._key] - ); - - // Remove the entries hash values. - let count = await client.DEL( - redisPrefix(`${this.constructor.name}_${this[this.constructor._key]}`) - ); - - // Return the number of removed values to the caller. - return this; - - } catch(error) { - throw error; - } - }; - - toJSON(){ - let result = {}; - for (const [key, value] of Object.entries(this)) { - if(this.constructor._keyMap[key] && this.constructor._keyMap[key].isPrivate) continue; - result[key] = value; - } - - return result - - // return JSON.stringify(result); - } - - toString(){ - return this[this.constructor._key]; - } - -} - - -module.exports = Table; \ No newline at end of file +module.exports = Table; From b78376cdf99367e78d8bc0783829fbef6b48d64b Mon Sep 17 00:00:00 2001 From: William Mantly Date: Wed, 31 Dec 2025 20:35:55 -0500 Subject: [PATCH 2/2] Add model-redis package references to documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added ORM reference in architecture.md (Redis section) - Added Data Models section in contributing.md with example - Updated README.md to mention model-redis in Architecture - Linked to https://www.npmjs.com/package/model-redis for more info 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- README.md | 2 +- docs/architecture.md | 2 ++ docs/contributing.md | 22 ++++++++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0f42ed8f..7bcfa88e 100755 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ The system consists of three main components: - Host lookup tree with wildcard matching - User authentication and authorization -3. **Redis** - Data store +3. **Redis** - Data store (using [model-redis](https://www.npmjs.com/package/model-redis) ORM) - Host configurations - User accounts and tokens - SSL certificate storage diff --git a/docs/architecture.md b/docs/architecture.md index 61f91ba3..b9f7d15d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -105,6 +105,8 @@ nodejs/ ### Redis (Data Store) +**ORM:** [model-redis](https://www.npmjs.com/package/model-redis) - A lightweight Redis ORM for Node.js with schema validation, relationships, and automatic key management. + **Stored Data:** - Host configurations (domain, IP, port, SSL settings) - User accounts and hashed passwords diff --git a/docs/contributing.md b/docs/contributing.md index 76ef5a38..dad2b1c9 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -248,6 +248,28 @@ When you open a PR: 4. Review from maintainers 5. Merge to master +## Data Models + +The project uses [model-redis](https://www.npmjs.com/package/model-redis) as the ORM for Redis data storage. All models extend the `Table` class and use a declarative schema via `_keyMap`. + +**Example Model:** +```javascript +const Table = require('../utils/redis_model'); + +class Host extends Table { + static _key = 'host'; // Primary key field + static _keyMap = { + 'host': {isRequired: true, type: 'string', min: 3, max: 500}, + 'ip': {isRequired: true, type: 'string', min: 3, max: 500}, + 'targetPort': {isRequired: true, type: 'number', min: 0, max: 65535}, + 'forcessl': {default: true, type: 'boolean'}, + 'created_on': {default: () => Date.now(), type: 'number'} + }; +} +``` + +**Learn more:** [model-redis documentation](https://www.npmjs.com/package/model-redis) + ## Adding Features ### Adding a DNS Provider