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.
- package/db.js +91 -69
- package/events.js +95 -12
- 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
|
-
|
|
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
|
-
|
|
98
|
+
let inputVal = data[key];
|
|
99
|
+
const type = rule.type || rule;
|
|
43
100
|
|
|
44
101
|
if (inputVal === undefined && rule.default !== undefined) {
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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 =
|
|
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
|