@ix-xs/node-comfort 1.0.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.
@@ -0,0 +1,350 @@
1
+ const FS = require("./FS");
2
+ const Checker = require("./Checker");
3
+ const { basename } = require("node:path");
4
+
5
+ const originalEmitWarning = process.emitWarning;
6
+ process.emitWarning = (warning, ...args) => {
7
+ if ((warning.name === "ExperimentalWarning" && warning.message.includes("SQLite")) || (typeof warning === "string" && warning.startsWith("SQLite is an experimental feature"))) {
8
+ return;
9
+ }
10
+
11
+ return originalEmitWarning.call(process, warning, ...args);
12
+ };
13
+
14
+ const { DatabaseSync } = require("node:sqlite");
15
+
16
+ /**
17
+ * @module Storage
18
+ */
19
+
20
+ module.exports = class Storage {
21
+ #$;
22
+ #statements = new Map();
23
+
24
+ /**
25
+ * @param {string} [path = ":memomry"]
26
+ */
27
+ constructor(path) {
28
+ if (path.includes("/")) {
29
+ const split = path.split("/");
30
+ const file = split[split.length - 1];
31
+ const folder = FS.createPath(path.replace(file, ""));
32
+
33
+ FS.createFolder(folder);
34
+ }
35
+
36
+ this.#$ = new DatabaseSync(path);
37
+
38
+ this.exec("PRAGMA journal_mode = WAL");
39
+ this.exec("PRAGMA foreign_keys = ON");
40
+ this.exec("PRAGMA synchronous = NORMAL");
41
+ this.exec("PRAGMA cache_size = -64000");
42
+ }
43
+
44
+ /**
45
+ * @param {string} sql
46
+ * @param {array|object} [options = {}]
47
+ */
48
+ exec(sql, options) {
49
+ try {
50
+ const stmt = this.#$.prepare(sql);
51
+ const result = stmt.run(...Checker.isArray(options) ? options : [options]);
52
+
53
+ return {
54
+ changes: result.changes,
55
+ lastInsertRowid: result.lastInsertRowid,
56
+ };
57
+
58
+ } catch (error) {
59
+ throw error;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * @param {string} sql
65
+ * @param {array|object} [options = []]
66
+ */
67
+ getOne(sql, options) {
68
+ try {
69
+ const stmt = this.#$.prepare(sql);
70
+
71
+ return stmt.get(...Checker.isArray(options) ? options : [options]) || null;
72
+ } catch (error) {
73
+ throw error;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * @param {string} sql
79
+ * @param {array|object} [options = []]
80
+ */
81
+ getAll(sql, options) {
82
+ try {
83
+ const stmt = this.#$.prepare(sql);
84
+
85
+ return stmt.all(...Checker.isArray(options) ? options : [options]) || [];
86
+ } catch (error) {
87
+ throw error;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * @param {string} name
93
+ * @param {string} sql
94
+ */
95
+ prepare(name, sql) {
96
+ if (this.#statements.has(name)) {
97
+ return this.#statements.get(name);
98
+ }
99
+
100
+ const stmt = this.#$.prepare(sql);
101
+
102
+ this.#statements.set(name, stmt);
103
+
104
+ return stmt;
105
+ }
106
+
107
+ /**
108
+ * @param {string} table
109
+ * @param {object} schema
110
+ */
111
+ createTable(table, schema) {
112
+ const columns = Object.entries(schema)
113
+ .map(([key, type]) => `${key} ${type}`)
114
+ .join(", ");
115
+
116
+ const sql = `CREATE TABLE IF NOT EXISTS ${table} (${columns})`;
117
+
118
+ this.exec(sql);
119
+ }
120
+
121
+ /**
122
+ * @param {string} table
123
+ * @param {object} data
124
+ */
125
+ insert(table, data) {
126
+ const keys = Object.keys(data);
127
+ const placeholders = keys.map(() => "?").join(", ");
128
+ const sql = `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders})`;
129
+
130
+ const result = this.exec(sql, Object.values(data));
131
+
132
+ return result.lastInsertRowid;
133
+ }
134
+
135
+ /**
136
+ * @param {string} table
137
+ * @param {number} id
138
+ * @returns {object|null}
139
+ */
140
+ findById(table, id) {
141
+ return this.getOne(`SELECT * FROM ${table} WHERE id = ?`, [id]);
142
+ }
143
+
144
+ /**
145
+ * @param {string} table
146
+ * @param {object} [where = {}]
147
+ * @param {object} [options = {}]
148
+ */
149
+ findAll(table, where, options) {
150
+ const { limit = 100, offset = 0, orderBy = "id" } = options;
151
+
152
+ let sql = `SELECT * FROM ${table}`;
153
+
154
+ if (Object.keys(where).length > 0) {
155
+ const conditions = Object.keys(where)
156
+ .map(key => `${key} = ?`)
157
+ .join(" AND ");
158
+ sql += ` WHERE ${conditions}`;
159
+ }
160
+
161
+ sql += ` ORDER BY ${orderBy} LIMIT ? OFFSET ?`;
162
+
163
+ const params = [...Object.values(where), limit, offset];
164
+
165
+ return this.getAll(sql, params);
166
+ }
167
+
168
+ /**
169
+ * @param {string} table
170
+ * @param {object} [where]
171
+ */
172
+ count(table, where = {}) {
173
+ let sql = `SELECT COUNT(*) as count FROM ${table}`;
174
+
175
+ if (Object.keys(where).length > 0) {
176
+ const conditions = Object.keys(where)
177
+ .map(key => `${key} = ?`)
178
+ .join(" AND ");
179
+ sql += ` WHERE ${conditions}`;
180
+ }
181
+
182
+ const result = this.getOne(sql, Object.values(where));
183
+
184
+ return result?.count || 0;
185
+ }
186
+
187
+ /**
188
+ * @param {string} table
189
+ * @param {number} id
190
+ * @param {object} data
191
+ */
192
+ update(table, id, data) {
193
+ const keys = Object.keys(data);
194
+ const setClause = keys.map(key => `${key} = ?`).join(", ");
195
+ const sql = `UPDATE ${table} SET ${setClause} WHERE id = ?`;
196
+
197
+ const result = this.exec(sql, [...Object.values(data), id]);
198
+
199
+ return result.changes;
200
+ }
201
+
202
+ /**
203
+ * @param {string} table
204
+ * @param {number} id
205
+ */
206
+ delete(table, id) {
207
+ const sql = `DELETE FROM ${table} WHERE id = ?`;
208
+ const result = this.exec(sql, [id]);
209
+
210
+ return result.changes;
211
+ }
212
+
213
+ /**
214
+ * @param {string} table
215
+ * @param {object} data
216
+ * @param {string} [conflictColumn]
217
+ */
218
+ upsert(table, data, conflictColumn = "id") {
219
+ const keys = Object.keys(data);
220
+ const placeholders = keys.map(() => "?").join(", ");
221
+ const updates = keys
222
+ .filter(k => k !== conflictColumn)
223
+ .map(k => `${k} = EXCLUDED.${k}`)
224
+ .join(", ");
225
+
226
+ const sql = `
227
+ INSERT INTO ${table} (${keys.join(", ")})
228
+ VALUES (${placeholders})
229
+ ON CONFLICT(${conflictColumn})
230
+ DO UPDATE SET ${updates}
231
+ `;
232
+
233
+ return this.exec(sql, Object.values(data));
234
+ }
235
+
236
+ /**
237
+ * @param {function} callback
238
+ */
239
+ transaction(callback) {
240
+ try {
241
+ this.exec("BEGIN TRANSACTION");
242
+
243
+ const result = callback();
244
+
245
+ this.exec("COMMIT");
246
+
247
+ return result;
248
+ } catch (error) {
249
+ this.exec("ROLLBACK");
250
+ throw error;
251
+ }
252
+ }
253
+
254
+ /**
255
+ * @param {string} migrationsDir - Répertoire contenant les migrations
256
+ */
257
+ runMigrations(migrationsDir) {
258
+ if (!FS.getFolder(migrationsDir)) {
259
+ FS.createFolder(migrationsDir);
260
+
261
+ return;
262
+ }
263
+
264
+ const migrationFiles = FS.getFilesIn(migrationsDir).filter(f => f.endsWith(".sql")).sort();
265
+
266
+ this.createTable("__migrations", {
267
+ name: "TEXT PRIMARY KEY",
268
+ executed_at: "DATETIME DEFAULT CURRENT_TIMESTAMP",
269
+ });
270
+
271
+ for (const file of migrationFiles) {
272
+ const migrationName = basename(file, ".sql");
273
+ const alreadyRun = this.getOne("SELECT * FROM __migrations WHERE name = ?", [migrationName]);
274
+
275
+ if (!alreadyRun) {
276
+ const sql = FS.readFile(`${migrationsDir}/${file}`);
277
+
278
+ try {
279
+ this.exec(sql);
280
+ this.insert("__migrations", { name: migrationName });
281
+ } catch (error) {
282
+ throw error;
283
+ }
284
+ }
285
+ }
286
+ }
287
+
288
+ /**
289
+ * @param {string} table
290
+ */
291
+ export(table) {
292
+ return this.getAll(`SELECT * FROM ${table}`);
293
+ }
294
+
295
+ /**
296
+ * @param {string} table
297
+ * @param {array} data
298
+ */
299
+ import(table, data) {
300
+ let count = 0;
301
+ this.transaction(() => {
302
+ for (const row of data) {
303
+ this.insert(table, row);
304
+ count++;
305
+ }
306
+ });
307
+ return count;
308
+ }
309
+
310
+ /**
311
+ * @param {string} table
312
+ */
313
+ truncate(table) {
314
+ this.exec(`DELETE FROM ${table}`);
315
+ }
316
+
317
+ /**
318
+ * @param {string} indexName
319
+ * @param {string} table
320
+ * @param {string} columns
321
+ */
322
+ createIndex(indexName, table, columns) {
323
+ this.exec(`CREATE INDEX IF NOT EXISTS ${indexName} ON ${table} (${columns})`);
324
+ }
325
+
326
+ /**
327
+ * @returns {object}
328
+ */
329
+ stats() {
330
+ const tables = this.getAll("SELECT name FROM sqlite_master WHERE type='table'");
331
+ const stats = {};
332
+
333
+ for (const { name } of tables) {
334
+ if (!name.startsWith("__")) {
335
+ const count = this.count(name);
336
+ stats[name] = count;
337
+ }
338
+ }
339
+
340
+ return stats;
341
+ }
342
+
343
+ close() {
344
+ if (this.db) {
345
+ this.#$.close();
346
+ this.#statements.clear();
347
+ this.#$ = null;
348
+ }
349
+ }
350
+ };
package/core/Utils.js ADDED
@@ -0,0 +1,206 @@
1
+ const { config } = require("@dotenvx/dotenvx");
2
+ const { log } = require("./Logger");
3
+
4
+ /**
5
+ * @module Utils
6
+ * @description
7
+ */
8
+ module.exports = {
9
+ getEnv(env) {
10
+ config({ quiet: true });
11
+ return process.env[env];
12
+ },
13
+ /**
14
+ * Creates a Promise that resolves after specified milliseconds.
15
+ *
16
+ * @param {number} ms - Milliseconds to wait (positive integer).
17
+ * @returns {Promise<void>} Promise that resolves after delay.
18
+ *
19
+ * @example
20
+ * // Sequential delays
21
+ * await wait(100); // 100ms
22
+ * await wait(500); // 500ms
23
+ * await wait(2000); // 2 seconds
24
+ */
25
+ wait(ms) {
26
+ return new Promise((resolve) => setTimeout(resolve, ms));
27
+ },
28
+ /**
29
+ *
30
+ * @param {boolean|Promise<boolean>} predicate
31
+ * @param {*} handler
32
+ * @param {object} [options]
33
+ * @param {number} [options.interval]
34
+ * @param {number|null} [options.timeout]
35
+ * @param {number|null} [options.max]
36
+ */
37
+ when(predicate, handler, options) {
38
+ const interval = options?.interval ?? 50;
39
+ const timeout = options?.timeout ?? null;
40
+ const max = options?.max ?? null;
41
+
42
+ const listeners = {
43
+ trigger: new Set(),
44
+ error: new Set(),
45
+ timeout: new Set(),
46
+ };
47
+
48
+ let _timer = null;
49
+ let _timeoutTimer = null;
50
+ let _stopped = false;
51
+ let _count = 0;
52
+
53
+ function emit(event, payload) {
54
+ for (const fn of listeners[event]) {
55
+ try {
56
+ fn(payload);
57
+ } catch (error) {
58
+ console.error(`[when] listener error on "${event}":`, error);
59
+ }
60
+ }
61
+ }
62
+
63
+ async function evalPredicate() {
64
+ if (typeof predicate === "function") {
65
+ return await predicate();
66
+ }
67
+ return Boolean(predicate);
68
+ }
69
+
70
+ async function tick() {
71
+ if (_stopped) return;
72
+
73
+ try {
74
+ const ok = await evalPredicate();
75
+
76
+ if (ok) {
77
+ _count++;
78
+ emit("trigger", handler);
79
+
80
+ if (max != null && _count >= max) {
81
+ _stopInternal();
82
+ return;
83
+ }
84
+ }
85
+
86
+ if (!_stopped) {
87
+ _timer = setTimeout(tick, interval);
88
+ }
89
+ } catch (error) {
90
+ emit("error", error);
91
+ if (!_stopped) {
92
+ _timer = setTimeout(tick, interval);
93
+ }
94
+ }
95
+ }
96
+
97
+ function _stopInternal() {
98
+ _stopped = true;
99
+ if (_timer) {
100
+ clearTimeout(_timer);
101
+ _timer = null;
102
+ }
103
+ if (_timeoutTimer) {
104
+ clearTimeout(_timeoutTimer);
105
+ _timeoutTimer = null;
106
+ }
107
+ }
108
+
109
+ const api = {
110
+ start() {
111
+ if (_stopped) return api;
112
+
113
+ if (!_timer) {
114
+ _timer = setTimeout(tick, 0);
115
+ }
116
+
117
+ if (timeout != null && !_timeoutTimer) {
118
+ _timeoutTimer = setTimeout(() => {
119
+ if (_stopped) return;
120
+ emit("timeout", handler);
121
+ _stopInternal();
122
+ }, timeout);
123
+ }
124
+
125
+ return api;
126
+ },
127
+ stop() {
128
+ _stopInternal();
129
+ return api;
130
+ },
131
+ /**
132
+ * @param {"error"|"trigger"|"timeout"} event
133
+ * @param {(payload: any) => void} fn
134
+ */
135
+ on(event, fn) {
136
+ listeners[event]?.add(fn);
137
+ return api;
138
+ },
139
+ /**
140
+ * @param {"error"|"trigger"|"timeout"} event
141
+ * @param {(payload: any) => void} fn
142
+ */
143
+ off(event, fn) {
144
+ listeners[event]?.delete(fn);
145
+ return api;
146
+ },
147
+ };
148
+
149
+ return api;
150
+ },
151
+ dontCrash() {
152
+ const defaults = {
153
+ error: (error) => {
154
+ log(
155
+ `<% red [@ix-xs/node-comfort] Process Error %>\n<% gray ${error.stack ?? error} %>`,
156
+ );
157
+ },
158
+ exit: (code) => {
159
+ log(
160
+ `<% yellow [@ix-xs/node-comfort] Process Exit %>\n→ Code: <% gray ${code} %>`,
161
+ );
162
+ },
163
+ sig: (signal) => {
164
+ log(
165
+ `<% yellow [@ix-xs/node-comfort] Process SIG* %>\n→ Signal: <% gray ${signal} %>`,
166
+ );
167
+
168
+ process.exit(0);
169
+ },
170
+ beforeExit: (code) => {},
171
+ };
172
+
173
+ const _ = {
174
+ /**
175
+ * @template { "error" | "exit" | "sig" | "beforeExit" } E
176
+ * @param { E } event
177
+ * @param { { error: (error: Error | unknown) => void | Promise<void>, exit: (code: number) => void | Promise<void>, sig: (signal: "SIGINT" | "SIGTERM" | "SIGQUIT") => void | Promise<void>, beforeExit: (code: number) => void|Promise<void> }[E] } handler
178
+ */
179
+ on(event, handler) {
180
+ if (event === "error") {
181
+ process.removeAllListeners("uncaughtException");
182
+ process.removeAllListeners("unhandledRejection");
183
+ process.on("uncaughtException", handler ?? defaults.error);
184
+ process.on("unhandledRejection", handler ?? defaults.error);
185
+ } else if (event === "exit") {
186
+ process.removeAllListeners("exit");
187
+ process.on("exit", handler ?? defaults.exit);
188
+ } else if (event === "sig") {
189
+ for (const signal of ["SIGINT", "SIGTERM", "SIGQUIT"]) {
190
+ process.removeAllListeners(signal);
191
+ process.on(signal, handler ?? defaults.sig);
192
+ }
193
+ } else if (event === "beforeExit") {
194
+ process.removeAllListeners("beforeExit");
195
+ process.on("beforeExit", handler ?? defaults.beforeExit);
196
+ }
197
+
198
+ return _;
199
+ },
200
+ };
201
+
202
+ _.on("error").on("exit").on("sig");
203
+
204
+ return _;
205
+ },
206
+ };
package/index.js ADDED
@@ -0,0 +1,17 @@
1
+ const Checker = require("./core/Checker");
2
+ const CLI = require("./core/CLI");
3
+ const Storage = require("./core/Storage");
4
+ const FS = require("./core/FS");
5
+ const Logger = require("./core/Logger");
6
+ const Stepper = require("./core/Stepper");
7
+ const Utils = require("./core/Utils");
8
+
9
+ module.exports = {
10
+ ...Checker,
11
+ ...CLI,
12
+ Storage,
13
+ ...FS,
14
+ ...Logger,
15
+ ...Stepper,
16
+ ...Utils,
17
+ };
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@ix-xs/node-comfort",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "keywords": [],
10
+ "author": "ix-xs",
11
+ "dependencies": {
12
+ "@dotenvx/dotenvx": "^1.51.2"
13
+ },
14
+ "engines": {
15
+ "node": ">=22.5.0"
16
+ }
17
+ }