goaded.utils 1.0.0 → 1.1.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 (3) hide show
  1. package/db.js +91 -69
  2. package/events.js +95 -12
  3. package/package.json +1 -1
package/db.js CHANGED
@@ -14,7 +14,6 @@ class Db {
14
14
  const db = this.db;
15
15
  const tableName = name.toLowerCase() + 's';
16
16
  const definition = schema.definition;
17
-
18
17
  const createTableSql = `
19
18
  CREATE TABLE IF NOT EXISTS ${tableName} (
20
19
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -24,46 +23,94 @@ class Db {
24
23
 
25
24
  const tableReady = new Promise((resolve, reject) => {
26
25
  db.run(createTableSql, (err) => {
27
- if (err) {
28
- console.error(`Error creating table ${tableName}:`, err.message);
29
- reject(err);
30
- } else {
31
- resolve();
32
- }
26
+ if (err) reject(err);
27
+ else resolve();
33
28
  });
34
29
  });
35
30
 
31
+ const hydrate = (value, type) => {
32
+ if (value === undefined || value === null) return value;
33
+
34
+ if (Array.isArray(type)) {
35
+ if (!Array.isArray(value)) return [];
36
+ const InnerType = type[0];
37
+ return value.map(item => hydrate(item, InnerType));
38
+ }
39
+
40
+ const isPrimitive = [String, Number, Boolean, Object, Date].includes(type);
41
+
42
+ if (!isPrimitive && typeof type === 'function') {
43
+ if (value instanceof type) return value;
44
+
45
+ try {
46
+ return new type(value);
47
+ } catch (e) {
48
+ console.warn(`Could not hydrate type ${type.name}`, e);
49
+ return value;
50
+ }
51
+ }
52
+
53
+ return value;
54
+ };
55
+
56
+ const validateValue = (key, value, rule) => {
57
+ const type = rule.type || rule;
58
+ if (rule.required && (value === undefined || value === null || value === '')) {
59
+ return `Path \`${key}\` is required.`;
60
+ }
61
+ if (value === undefined || value === null) return null;
62
+
63
+ if (Array.isArray(type)) {
64
+ if (!Array.isArray(value)) return `Path \`${key}\` must be an Array.`;
65
+ const InnerType = type[0];
66
+ for (let i = 0; i < value.length; i++) {
67
+ const error = validateValue(`${key}[${i}]`, value[i], { ...rule, type: InnerType, required: false });
68
+ if (error) return error;
69
+ }
70
+ return null;
71
+ }
72
+
73
+ if (type === String && typeof value !== 'string') return `Path \`${key}\` must be a String`;
74
+ if (type === Number && isNaN(value)) return `Path \`${key}\` must be a Number`;
75
+ if (type === Boolean && typeof value !== 'boolean') return `Path \`${key}\` must be a Boolean`;
76
+
77
+ const isPrimitive = [String, Number, Boolean, Object, Date].includes(type);
78
+ if (!isPrimitive && typeof type === 'function') {
79
+ if (!(value instanceof type)) {
80
+ if (value && typeof value === 'object' && typeof type.validate === 'function') {
81
+ try { type.validate(value); }
82
+ catch(e) { return `Validation failed in nested ${type.name}: ${e.message}`; }
83
+ } else {
84
+ return `Path \`${key}\` must be instance of ${type.name}`;
85
+ }
86
+ }
87
+ }
88
+
89
+ return null;
90
+ };
91
+
36
92
  return class Model {
37
93
  constructor(data) {
38
- this._data = {};
94
+ this._data = {};
39
95
 
40
96
  Object.keys(definition).forEach(key => {
41
97
  const rule = definition[key];
42
- const inputVal = data[key];
98
+ let inputVal = data[key];
99
+ const type = rule.type || rule;
43
100
 
44
101
  if (inputVal === undefined && rule.default !== undefined) {
45
- this._data[key] = typeof rule.default === 'function' ? rule.default() : rule.default;
46
- } else if (inputVal !== undefined) {
47
- this._data[key] = inputVal;
102
+ inputVal = typeof rule.default === 'function' ? rule.default() : rule.default;
103
+ }
104
+ if (inputVal !== undefined) {
105
+ this._data[key] = hydrate(inputVal, type);
48
106
  }
49
107
  });
50
-
51
- if(data.id) this._data.id = data.id;
52
- }
53
108
 
54
- JSONify() {
55
- console.log(this._data);
56
- return this._data;
57
- }
58
-
59
- toJSON() {
60
- return this._data;
61
- }
109
+ if(data.id) this._data.id = data.id;
62
110
 
63
- static createProxy(instance) {
64
- return new Proxy(instance, {
111
+ return new Proxy(this, {
65
112
  get(target, prop) {
66
- if (prop in target) return target[prop];
113
+ if (prop in target) return target[prop];
67
114
  return target._data[prop];
68
115
  },
69
116
  set(target, prop, value) {
@@ -71,44 +118,23 @@ class Db {
71
118
  target._data[prop] = value;
72
119
  return true;
73
120
  }
74
- return true;
121
+ return true;
75
122
  }
76
123
  });
77
124
  }
78
125
 
126
+ toJSON() {
127
+ return this._data;
128
+ }
129
+
79
130
  static validate(dataObject) {
80
131
  const errors = {};
81
132
  Object.keys(definition).forEach(key => {
82
- let value = dataObject[key];
83
- const rules = definition[key];
84
-
85
- if (typeof value === 'string') {
86
- if (rules.trim) value = value.trim();
87
- if (rules.lowercase) value = value.toLowerCase();
88
- if (rules.uppercase) value = value.toUpperCase();
89
- dataObject[key] = value;
90
- }
91
-
92
- if (rules.required && (value === undefined || value === null || value === '')) {
93
- errors[key] = `Path \`${key}\` is required.`;
94
- return;
95
- }
96
-
97
- if (value === undefined || value === null) return;
98
-
99
- if (rules.type === Number && isNaN(value)) errors[key] = "Not a number";
100
- if (rules.type === Boolean && typeof value !== 'boolean') errors[key] = "Not a boolean";
101
-
102
- if (rules.type === String) {
103
- if (rules.enum && !rules.enum.includes(value))
104
- errors[key] = `\`${value}\` is not allowed.`;
105
- if (rules.match && !rules.match.test(value))
106
- errors[key] = "Regex validation failed";
107
- }
108
- if (rules.type === Number) {
109
- if (rules.min !== undefined && value < rules.min) errors[key] = `Min ${rules.min}`;
110
- if (rules.max !== undefined && value > rules.max) errors[key] = `Max ${rules.max}`;
111
- }
133
+ const rule = definition[key];
134
+ const value = dataObject._data ? dataObject._data[key] : dataObject[key];
135
+
136
+ const error = validateValue(key, value, rule);
137
+ if (error) errors[key] = error;
112
138
  });
113
139
 
114
140
  if (Object.keys(errors).length > 0) throw { message: "Validation failed", errors };
@@ -116,9 +142,8 @@ class Db {
116
142
 
117
143
  async save() {
118
144
  await tableReady;
119
-
120
145
  try {
121
- Model.validate(this._data);
146
+ Model.validate(this);
122
147
  } catch (e) {
123
148
  return Promise.reject(e);
124
149
  }
@@ -128,18 +153,16 @@ class Db {
128
153
  const self = this;
129
154
 
130
155
  if (this._data.id) {
131
- const sql = `UPDATE ${tableName} SET info = ? WHERE id = ?`;
132
- db.run(sql, [jsonPayload, this._data.id], function(err) {
156
+ db.run(`UPDATE ${tableName} SET info = ? WHERE id = ?`, [jsonPayload, this._data.id], (err) => {
133
157
  if(err) reject(err);
134
158
  else resolve(self);
135
159
  });
136
160
  } else {
137
- const sql = `INSERT INTO ${tableName} (info) VALUES (?)`;
138
- db.run(sql, [jsonPayload], function(err) {
161
+ db.run(`INSERT INTO ${tableName} (info) VALUES (?)`, [jsonPayload], function(err) {
139
162
  if (err) reject(err);
140
163
  else {
141
164
  self._data.id = this.lastID;
142
- resolve(Model.createProxy(self));
165
+ resolve(self);
143
166
  }
144
167
  });
145
168
  }
@@ -161,9 +184,7 @@ class Db {
161
184
  }).filter(x => x).join(' AND ');
162
185
 
163
186
  if(clauses) sql += ` WHERE ${clauses}`;
164
- keys.forEach(key => {
165
- if(definition[key] || key === 'id') params.push(query[key]);
166
- });
187
+ keys.forEach(key => params.push(query[key]));
167
188
  }
168
189
 
169
190
  db.all(sql, params, (err, rows) => {
@@ -172,7 +193,7 @@ class Db {
172
193
  const results = rows.map(row => {
173
194
  const data = JSON.parse(row.info);
174
195
  data.id = row.id;
175
- return Model.createProxy(new Model(data));
196
+ return new Model(data);
176
197
  });
177
198
  resolve(results);
178
199
  }
@@ -181,10 +202,11 @@ class Db {
181
202
  }
182
203
 
183
204
  static async create(data) {
184
- const instance = Model.createProxy(new Model(data));
205
+ const instance = new Model(data);
185
206
  return await instance.save();
186
207
  }
187
208
  };
188
209
  }
189
210
  }
211
+
190
212
  module.exports = Db;
package/events.js CHANGED
@@ -1,20 +1,52 @@
1
+ /**
2
+ * A simple event emitter class that manages event subscriptions and emissions.
3
+ * Supports synchronous and asynchronous execution, one-time events, and repeated emissions.
4
+ */
1
5
  class emitter {
6
+ /**
7
+ * Internal storage for event listeners.
8
+ * @type {Array<{event: string, callback: Function}>}
9
+ */
2
10
  listeners = [];
3
-
11
+
12
+ /**
13
+ * Creates a new instance of the emitter.
14
+ */
4
15
  constructor() {}
5
-
16
+
17
+ /**
18
+ * Subscribes a callback function to a specific event.
19
+ *
20
+ * @param {string} event - The name of the event to subscribe to.
21
+ * @param {Function} callback - The function to execute when the event is emitted.
22
+ * @returns {emitter} The current instance for chaining.
23
+ */
6
24
  on(event, callback) {
7
25
  this.listeners.push({event, callback});
8
26
  return this;
9
27
  }
10
-
28
+
29
+ /**
30
+ * Unsubscribes a specific callback from an event.
31
+ *
32
+ * @param {string} event - The name of the event.
33
+ * @param {Function} callback - The specific callback function to remove.
34
+ * @returns {emitter} The current instance for chaining.
35
+ */
11
36
  off(event, callback) {
12
37
  this.listeners = this.listeners.filter(
13
38
  listener => listener.event !== event || listener.callback !== callback
14
39
  );
15
40
  return this;
16
41
  }
17
-
42
+
43
+ /**
44
+ * Subscribes to an event only once. The callback is removed after the first execution.
45
+ *
46
+ * @param {string} event - The name of the event.
47
+ * @param {Function} callback - The function to execute once.
48
+ * @returns {emitter} The current instance for chaining.
49
+ */
18
50
  once(event, callback) {
19
51
  const wrappedCallback = (...args) => {
20
52
  callback(...args);
@@ -23,7 +55,15 @@ class emitter {
23
55
  this.on(event, wrappedCallback);
24
56
  return this;
25
57
  }
26
-
58
+
59
+ /**
60
+ * Synchronously calls all listeners registered for the specified event.
61
+ * Errors in listeners are caught and logged, preventing the loop from breaking.
62
+ *
63
+ * @param {string} event - The name of the event to emit.
64
+ * @param {...*} args - Arguments to pass to the callback functions.
65
+ * @returns {void}
66
+ */
27
67
  emit(event, ...args) {
28
68
  this.listeners.forEach(listener => {
29
69
  if(listener.event === event) {
@@ -35,7 +75,16 @@ class emitter {
35
75
  }
36
76
  });
37
77
  }
38
-
78
+
79
+ /**
80
+ * Asynchronously calls all listeners registered for the specified event.
81
+ * Waits for all promises returned by listeners to resolve.
82
+ *
83
+ * @async
84
+ * @param {string} event - The name of the event to emit.
85
+ * @param {...*} args - Arguments to pass to the callback functions.
86
+ * @returns {Promise<void>} A promise that resolves when all listeners have completed.
87
+ */
39
88
  async emitAsync(event, ...args) {
40
89
  const promises = this.listeners
41
90
  .filter(listener => listener.event === event)
@@ -43,7 +92,15 @@ class emitter {
43
92
 
44
93
  await Promise.all(promises);
45
94
  }
46
-
95
+
96
+ /**
97
+ * Synchronously calls all listeners for an event and then immediately removes them.
98
+ * This effectively "flushes" the listeners for that specific event.
99
+ *
100
+ * @param {string} event - The name of the event.
101
+ * @param {...*} args - Arguments to pass to the callback functions.
102
+ * @returns {void}
103
+ */
47
104
  emitOnce(event, ...args) {
48
105
  this.listeners = this.listeners.filter(listener => {
49
106
  if(listener.event === event) {
@@ -52,12 +109,20 @@ class emitter {
52
109
  } catch(error) {
53
110
  console.error(`Error in event listener for "${event}":`, error);
54
111
  }
55
- return false;
112
+ return false; // Remove listener
56
113
  }
57
- return true;
114
+ return true; // Keep listener
58
115
  });
59
116
  }
60
-
117
+
118
+ /**
119
+ * Asynchronously calls all listeners for an event and then removes them.
120
+ *
121
+ * @async
122
+ * @param {string} event - The name of the event.
123
+ * @param {...*} args - Arguments to pass to the callback functions.
124
+ * @returns {Promise<void>} A promise that resolves when all listeners have completed.
125
+ */
61
126
  async emitOnceAsync(event, ...args) {
62
127
  const matchingListeners = this.listeners.filter(
63
128
  listener => listener.event === event
@@ -73,7 +138,15 @@ class emitter {
73
138
  )
74
139
  );
75
140
  }
76
-
141
+
142
+ /**
143
+ * Synchronously triggers listeners for an event a specific number of times.
144
+ *
145
+ * @param {string} event - The name of the event.
146
+ * @param {number} times - The number of times to trigger the listeners.
147
+ * @param {...*} args - Arguments to pass to the callback functions.
148
+ * @returns {void}
149
+ */
77
150
  emitRepeat(event, times, ...args) {
78
151
  this.listeners.forEach(listener => {
79
152
  if(listener.event === event) {
@@ -87,7 +160,17 @@ class emitter {
87
160
  }
88
161
  });
89
162
  }
90
-
163
+
164
+ /**
165
+ * Asynchronously triggers listeners for an event a specific number of times.
166
+ * It waits for all listeners to complete one round before starting the next iteration.
167
+ *
168
+ * @async
169
+ * @param {string} event - The name of the event.
170
+ * @param {number} times - The number of times to trigger the listeners.
171
+ * @param {...*} args - Arguments to pass to the callback functions.
172
+ * @returns {Promise<void>} A promise that resolves when all repetitions are complete.
173
+ */
91
174
  async emitRepeatAsync(event, times, ...args) {
92
175
  const matchingListeners = this.listeners.filter(
93
176
  listener => listener.event === event
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "sqlite3": "^5.1.7"
4
4
  },
5
5
  "name": "goaded.utils",
6
- "version": "1.0.0",
6
+ "version": "1.1.0",
7
7
  "description": "",
8
8
  "main": "services.js",
9
9
  "devDependencies": {},