lamix 4.2.23 → 4.2.25

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/README.md CHANGED
@@ -313,6 +313,8 @@ class User extends BaseModel {
313
313
  saveUninitialized: false,
314
314
  store: new LamixSessionStore({
315
315
  ttl: 60 * 60 * 24, // 1 day
316
+ cleanupInterval: 60000, // 60 seconds
317
+ lazyInit: true, // // default false
316
318
  }),
317
319
  cookie: {
318
320
  httpOnly: true,
@@ -499,14 +499,24 @@ export class DBError extends Error {
499
499
  }
500
500
  export class LamixSessionStore {
501
501
  constructor(options?: {});
502
+ table: any;
502
503
  ttl: any;
503
504
  cleanupInterval: any;
505
+ compress: boolean;
506
+ lazyInit: any;
507
+ cache: LRUCache<{}, {}, unknown>;
508
+ _ensureTable(): Promise<void>;
509
+ _compress(data: any): Promise<Buffer<any>>;
510
+ _decompress(buffer: any): Promise<any>;
511
+ _getExpiry(sessionData: any): number;
512
+ _cacheTTL(expires: any): number;
504
513
  get(sid: any, cb: any): Promise<any>;
505
514
  set(sid: any, sessionData: any, cb: any): Promise<void>;
506
515
  destroy(sid: any, cb: any): Promise<void>;
507
516
  touch(sid: any, sessionData: any, cb: any): Promise<any>;
508
517
  _startCleanup(): void;
509
518
  _cleanupTimer: NodeJS.Timeout;
519
+ close(): void;
510
520
  }
511
521
  export class BaseModel extends Model {
512
522
  static passwordField: string;
@@ -575,7 +585,7 @@ declare class SimpleCache {
575
585
  maxSize: number;
576
586
  };
577
587
  }
578
- import { AsyncLocalStorage } from "node:async_hooks";
588
+ import { AsyncLocalStorage } from "async_hooks";
579
589
  declare class HasManyThrough extends Relation {
580
590
  constructor(parent: any, relatedClass: any, throughClass: any, firstKey: any, secondKey: any, localKey: any, secondLocalKey: any);
581
591
  throughClass: any;
@@ -607,7 +617,7 @@ declare class MorphToMany extends Relation {
607
617
  }
608
618
  declare class MorphedByMany extends MorphToMany {
609
619
  }
610
- import util = require("util");
620
+ import util = require("node:util");
611
621
  declare class Paginator {
612
622
  constructor(data: any, total: any, page: any, perPage: any);
613
623
  data: any;
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const util = require('util');
3
+ const util = require('node:util');
4
4
  const { performance } = require('perf_hooks');
5
5
  const { AsyncLocalStorage } = require('async_hooks');
6
6
  require('dotenv').config();
@@ -382,13 +382,13 @@ class DB {
382
382
 
383
383
  static select(sql, bindings = []) { return this.raw(sql, bindings); }
384
384
  static statement(sql, bindings = []) { return this.affectingStatement(sql, bindings); }
385
- static insert(sql, bindings = []) {
386
- return this.affectingStatement(sql, bindings)
387
- .then(r => r.insertId ?? true);
385
+ static async insert(sql, bindings = []) {
386
+ const r = await this.affectingStatement(sql, bindings);
387
+ return r.insertId ?? true;
388
388
  }
389
- static update(sql, bindings = []) {
390
- return this.affectingStatement(sql, bindings)
391
- .then(r => r.affectedRows ?? 0);
389
+ static async update(sql, bindings = []) {
390
+ const r = await this.affectingStatement(sql, bindings);
391
+ return r.affectedRows ?? 0;
392
392
  }
393
393
  static delete(sql, bindings = []) { return this.update(sql, bindings); }
394
394
 
@@ -459,6 +459,222 @@ class DB {
459
459
 
460
460
  const escapeId = s => DB.escapeId(s);
461
461
 
462
+ class LamixSessionStore extends session.Store {
463
+ constructor(options = {}) {
464
+ super();
465
+
466
+ this.table = options.table || 'sessions';
467
+ this.ttl = options.ttl || 86400; // seconds
468
+ this.cleanupInterval = options.cleanupInterval || 60000; // ms
469
+ this.compress = options.compress !== false; // default true
470
+ this.lazyInit = options.lazyInit || false;
471
+
472
+ const {
473
+ cacheSizeMB = 50,
474
+ cacheTTL = 1000 * 60 * 5
475
+ } = options;
476
+
477
+ this.cache = new LRUCache({
478
+ maxSize: cacheSizeMB * 1024 * 1024,
479
+ ttl: cacheTTL,
480
+ ttlAutopurge: true,
481
+ sizeCalculation: (value) => {
482
+ try { return Buffer.byteLength(JSON.stringify(value)); }
483
+ catch { return 1024; }
484
+ }
485
+ });
486
+
487
+ if (!this.lazyInit) {
488
+ this._ensureTable().catch(err => {
489
+ console.error('Session table init failed:', err);
490
+ });
491
+ }
492
+
493
+ this._startCleanup();
494
+ }
495
+
496
+ /* ---------- Ensure Table ---------- */
497
+
498
+ async _ensureTable() {
499
+ const table = DB.escapeId(this.table);
500
+
501
+ if (DB.driver === 'pg') {
502
+ await DB.statement(`
503
+ CREATE TABLE IF NOT EXISTS ${table} (
504
+ sid TEXT PRIMARY KEY,
505
+ data TEXT NOT NULL,
506
+ expires BIGINT NOT NULL
507
+ )
508
+ `);
509
+ } else if (DB.driver === 'sqlite') {
510
+ await DB.statement(`
511
+ CREATE TABLE IF NOT EXISTS ${table} (
512
+ sid TEXT PRIMARY KEY,
513
+ data TEXT NOT NULL,
514
+ expires INTEGER NOT NULL
515
+ )
516
+ `);
517
+ } else {
518
+ await DB.statement(`
519
+ CREATE TABLE IF NOT EXISTS ${table} (
520
+ sid VARCHAR(255) PRIMARY KEY,
521
+ data TEXT NOT NULL,
522
+ expires BIGINT NOT NULL
523
+ )
524
+ `);
525
+ }
526
+ }
527
+
528
+ /* ---------- Utilities ---------- */
529
+
530
+ async _compress(data) {
531
+ if (!this.compress) return Buffer.from(data);
532
+ return util.promisify(zlib.deflate)(data);
533
+ }
534
+
535
+ async _decompress(buffer) {
536
+ try {
537
+ if (!this.compress) return buffer;
538
+ return await util.promisify(zlib.inflate)(buffer);
539
+ } catch {
540
+ // fallback for old non-compressed sessions
541
+ return buffer;
542
+ }
543
+ }
544
+
545
+ _getExpiry(sessionData) {
546
+ return sessionData.cookie?.expires
547
+ ? new Date(sessionData.cookie.expires).getTime()
548
+ : Date.now() + this.ttl * 1000;
549
+ }
550
+
551
+ _cacheTTL(expires) {
552
+ const ms = expires - Date.now();
553
+ return ms > 0 ? ms : 1;
554
+ }
555
+
556
+ /* ---------- Core ---------- */
557
+
558
+ async get(sid, cb) {
559
+ try {
560
+ const cached = this.cache.get(sid);
561
+ if (cached) return cb(null, cached);
562
+
563
+ const rows = await DB.select(
564
+ `SELECT data, expires FROM ${DB.escapeId(this.table)} WHERE sid = ? AND expires > ?`,
565
+ [sid, Date.now()]
566
+ );
567
+
568
+ if (!rows.length) return cb(null, null);
569
+
570
+ const buffer = Buffer.from(rows[0].data, 'base64');
571
+ const decompressed = await this._decompress(buffer);
572
+
573
+ const sessionData = JSON.parse(decompressed.toString());
574
+
575
+ this.cache.set(sid, sessionData, { ttl: this._cacheTTL(rows[0].expires) });
576
+
577
+ cb(null, sessionData);
578
+ } catch (err) {
579
+ cb(err);
580
+ }
581
+ }
582
+
583
+ async set(sid, sessionData, cb) {
584
+ try {
585
+ const expires = this._getExpiry(sessionData);
586
+ const raw = JSON.stringify(sessionData);
587
+ const compressed = await this._compress(raw);
588
+ const data = compressed.toString('base64');
589
+
590
+ const table = DB.escapeId(this.table);
591
+
592
+ if (DB.driver === 'pg') {
593
+ await DB.statement(`
594
+ INSERT INTO ${table} (sid, data, expires)
595
+ VALUES (?, ?, ?)
596
+ ON CONFLICT (sid)
597
+ DO UPDATE SET data = EXCLUDED.data, expires = EXCLUDED.expires
598
+ `, [sid, data, expires]);
599
+
600
+ } else if (DB.driver === 'sqlite') {
601
+ await DB.statement(`
602
+ INSERT INTO ${table} (sid, data, expires)
603
+ VALUES (?, ?, ?)
604
+ ON CONFLICT(sid)
605
+ DO UPDATE SET data=excluded.data, expires=excluded.expires
606
+ `, [sid, data, expires]);
607
+
608
+ } else {
609
+ await DB.statement(`
610
+ INSERT INTO ${table} (sid, data, expires)
611
+ VALUES (?, ?, ?)
612
+ ON DUPLICATE KEY UPDATE data=VALUES(data), expires=VALUES(expires)
613
+ `, [sid, data, expires]);
614
+ }
615
+
616
+ this.cache.set(sid, sessionData, { ttl: this._cacheTTL(expires) });
617
+
618
+ cb(null);
619
+ } catch (err) {
620
+ cb(err);
621
+ }
622
+ }
623
+
624
+ async destroy(sid, cb) {
625
+ try {
626
+ await DB.delete(
627
+ `DELETE FROM ${DB.escapeId(this.table)} WHERE sid = ?`,
628
+ [sid]
629
+ );
630
+ this.cache.delete(sid);
631
+ cb(null);
632
+ } catch (err) {
633
+ cb(err);
634
+ }
635
+ }
636
+
637
+ async touch(sid, sessionData, cb) {
638
+ if (!sessionData) return cb();
639
+
640
+ try {
641
+ const expires = this._getExpiry(sessionData);
642
+
643
+ await DB.update(
644
+ `UPDATE ${DB.escapeId(this.table)} SET expires = ? WHERE sid = ?`,
645
+ [expires, sid]
646
+ );
647
+
648
+ this.cache.set(sid, sessionData, { ttl: this._cacheTTL(expires) });
649
+
650
+ cb();
651
+ } catch (err) {
652
+ cb(err);
653
+ }
654
+ }
655
+
656
+ /* ---------- Cleanup ---------- */
657
+
658
+ _startCleanup() {
659
+ this._cleanupTimer = setInterval(async () => {
660
+ try {
661
+ await DB.delete(
662
+ `DELETE FROM ${DB.escapeId(this.table)} WHERE expires < ?`,
663
+ [Date.now()]
664
+ );
665
+ } catch {}
666
+ }, this.cleanupInterval);
667
+
668
+ this._cleanupTimer.unref();
669
+ }
670
+
671
+ close() {
672
+ if (this._cleanupTimer) {
673
+ clearInterval(this._cleanupTimer);
674
+ }
675
+ }
676
+ }
677
+
462
678
 
463
679
  // -----------------------------
464
680
  // MAIN Validator
@@ -4028,204 +4244,6 @@ class Model {
4028
4244
  }
4029
4245
  }
4030
4246
 
4031
- /* -------------------- DB Model -------------------- */
4032
- class Session extends Model {
4033
- static table = 'sessions';
4034
- static primaryKey = 'sid';
4035
- static slugKey = null;
4036
- static timestamps = false;
4037
-
4038
- static fillable = ['sid', 'data', 'expires'];
4039
- }
4040
-
4041
- /* -------------------- Optional Session Model -------------------- */
4042
- let SessionModel = null;
4043
-
4044
- try {
4045
- SessionModel = Session;
4046
- } catch (err) {
4047
- SessionModel = null;
4048
- }
4049
-
4050
- /* -------------------- Store -------------------- */
4051
- const { promisify } = require('util');
4052
-
4053
- const gzip = promisify(zlib.gzip);
4054
- const gunzip = promisify(zlib.gunzip);
4055
-
4056
- class LamixSessionStore extends session.Store {
4057
- constructor(options = {}) {
4058
- super();
4059
-
4060
- this.ttl = options.ttl || 86400; // seconds
4061
- this.cleanupInterval = options.cleanupInterval || 60000;
4062
-
4063
- this._startCleanup();
4064
- }
4065
-
4066
- /* ---------- Get ---------- */
4067
-
4068
- async get(sid, cb) {
4069
- try {
4070
- const now = Date.now();
4071
-
4072
- const row = await Session
4073
- .query()
4074
- .where('sid', sid)
4075
- .where('expires', '>', now)
4076
- .first();
4077
-
4078
- if (!row) return cb(null, null);
4079
-
4080
- let sessionJSON;
4081
-
4082
- // ---------- Step 1: Decompress ----------
4083
- try {
4084
- const buffer = Buffer.from(row.data, 'base64');
4085
- const decompressed = await gunzip(buffer);
4086
- sessionJSON = decompressed.toString('utf8');
4087
- } catch {
4088
- // fallback to legacy plain JSON
4089
- sessionJSON = row.data;
4090
- }
4091
-
4092
- // ---------- Step 2: Parse JSON safely ----------
4093
- try {
4094
- const parsed = JSON.parse(sessionJSON);
4095
- return cb(null, parsed);
4096
- } catch (parseErr) {
4097
- console.warn('Corrupted session detected. Destroying:', sid);
4098
-
4099
- // destroy corrupted session
4100
- await Session
4101
- .query()
4102
- .where('sid', sid)
4103
- .delete();
4104
-
4105
- return cb(null, null); // treat as no session
4106
- }
4107
-
4108
- } catch (err) {
4109
- console.error('Session GET failed:', err);
4110
-
4111
- // IMPORTANT: never pass fatal error to express-session
4112
- return cb(null, null);
4113
- }
4114
- }
4115
-
4116
- /* ---------- Set ---------- */
4117
-
4118
- async set(sid, sessionData, cb) {
4119
- try {
4120
- const expires =
4121
- sessionData.cookie?.expires
4122
- ? new Date(sessionData.cookie.expires).getTime()
4123
- : Date.now() + this.ttl * 1000;
4124
-
4125
- const json = JSON.stringify(sessionData);
4126
-
4127
- // Async compression (non-blocking)
4128
- const compressed = await gzip(json);
4129
- const base64Data = compressed.toString('base64');
4130
-
4131
- const payload = {
4132
- sid,
4133
- data: base64Data,
4134
- expires
4135
- };
4136
-
4137
- const existing = await Session
4138
- .query()
4139
- .where('sid', sid)
4140
- .first();
4141
-
4142
- if (existing) {
4143
- await existing.update(payload);
4144
- } else {
4145
- const session = new Session(payload, false);
4146
- await session.saveNew(payload);
4147
- }
4148
-
4149
- cb(null);
4150
- } catch (err) {
4151
- cb(new DBError('Failed to persist session', {
4152
- sid,
4153
- operation: 'set',
4154
- err
4155
- }));
4156
- }
4157
- }
4158
-
4159
- /* ---------- Destroy ---------- */
4160
-
4161
- async destroy(sid, cb) {
4162
- try {
4163
- await Session
4164
- .query()
4165
- .where('sid', sid)
4166
- .delete();
4167
-
4168
- cb(null);
4169
- } catch (err) {
4170
- cb(new DBError('Failed to destroy session', {
4171
- sid,
4172
- operation: 'destroy',
4173
- err
4174
- }));
4175
- }
4176
- }
4177
-
4178
- /* ---------- Touch ---------- */
4179
-
4180
- async touch(sid, sessionData, cb) {
4181
- if (!sessionData) return cb(null);
4182
-
4183
- try {
4184
- const expires =
4185
- sessionData.cookie?.expires
4186
- ? new Date(sessionData.cookie.expires).getTime()
4187
- : Date.now() + this.ttl * 1000;
4188
-
4189
- await Session
4190
- .query()
4191
- .where('sid', sid)
4192
- .update({ expires });
4193
-
4194
- cb();
4195
- } catch (err) {
4196
- cb(new DBError('Failed to touch session', {
4197
- sid,
4198
- operation: 'touch',
4199
- err
4200
- }));
4201
- }
4202
- }
4203
-
4204
- /* ---------- Cleanup ---------- */
4205
-
4206
- _startCleanup() {
4207
- this._cleanupTimer = setInterval(async () => {
4208
- try {
4209
- await Session
4210
- .query()
4211
- .where('expires', '<', Date.now())
4212
- .delete();
4213
- } catch (err) {
4214
- console.error(
4215
- new DBError('Session cleanup failed', {
4216
- operation: 'cleanup',
4217
- err
4218
- })
4219
- );
4220
- }
4221
- }, this.cleanupInterval);
4222
-
4223
- this._cleanupTimer.unref();
4224
- }
4225
- }
4226
-
4227
-
4228
-
4229
4247
  // --- BaseModel with bcrypt hashing ---
4230
4248
  const bcrypt = tryRequire('bcrypt');
4231
4249
  class BaseModel extends Model {
@@ -4314,4 +4332,13 @@ class BaseModel extends Model {
4314
4332
  }
4315
4333
  }
4316
4334
 
4335
+ // ======================
4336
+ // Database Init
4337
+ // ======================
4338
+ DB.initFromEnv();
4339
+
4340
+ (async () => {
4341
+ await DB.connect();
4342
+ })();
4343
+
4317
4344
  module.exports = { DB, Model, Validator, ValidationError, Collection, QueryBuilder, HasMany, HasOne, BelongsTo, BelongsToMany, DBError, LamixSessionStore, BaseModel};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lamix",
3
- "version": "4.2.23",
3
+ "version": "4.2.25",
4
4
  "description": "lamix - ORM for Node-express js",
5
5
  "main": "./lib/index.js",
6
6
  "type": "commonjs",
@@ -25,14 +25,14 @@
25
25
  "build": "tsc && echo 'Build complete'"
26
26
  },
27
27
  "devDependencies": {
28
- "@types/node": "^25.0.3",
29
- "typescript": "^5.9.3"
28
+ "@types/node": "^20.11.30",
29
+ "typescript": "^5.4.5",
30
+ "chalk": "^4.1.2"
30
31
  },
31
32
  "dependencies": {
32
33
  "bcrypt": "^6.0.0",
33
34
  "express-session": "^1.19.0",
34
35
  "lru-cache": "^11.2.5",
35
- "chalk": "^4.1.2",
36
36
  "dotenv": "^17.2.4 "
37
37
  },
38
38
  "keywords": [