lamix 4.2.13 → 4.2.15

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
@@ -288,21 +288,9 @@ class User extends BaseModel {
288
288
  return this.hasMany(Post', 'user_id', 'id').onDelete('cascade');
289
289
  }
290
290
 
291
- # Create sessions Table MySQL/Postgres sql
292
- CREATE TABLE sessions (
293
- sid VARCHAR(255) PRIMARY KEY,
294
- data TEXT NOT NULL,
295
- expires BIGINT NOT NULL
296
- );
297
- CREATE INDEX idx_sessions_expires ON sessions (expires);
298
-
299
- # Create sessions Table sqlite sql
300
- CREATE TABLE sessions (
301
- sid TEXT PRIMARY KEY,
302
- data TEXT NOT NULL,
303
- expires INTEGER NOT NULL
304
- );
305
- CREATE INDEX idx_sessions_expires ON sessions (expires);
291
+ # migrate sessions Table(whenever migration is run session is auto generated if missing)
292
+ npx lamix migrate
293
+ ➡️ sessions table + index are guaranteed to exist.
306
294
 
307
295
  # Session setup
308
296
  const express = require('express');
@@ -340,4 +328,4 @@ class User extends BaseModel {
340
328
  });
341
329
 
342
330
  app.listen(3000);
343
- }
331
+ }
package/bin/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
@@ -274,6 +274,8 @@ async function runMigrations() {
274
274
  DB.initFromEnv();
275
275
  await DB.connect();
276
276
 
277
+ await ensureSessionsTable();
278
+
277
279
  const applied = await getAppliedMigrations();
278
280
  const files = fs.readdirSync(MIGRATIONS_DIR)
279
281
  .filter(f => f.endsWith('.js'))
@@ -348,6 +350,63 @@ async function tableExists(tableName) {
348
350
  return false;
349
351
  }
350
352
 
353
+ // ------------------ SESSION hELPER ------------------
354
+
355
+ async function ensureSessionsTable() {
356
+ const exists = await tableExists('sessions');
357
+ if (exists) {
358
+ log.info('ℹ️ sessions table already exists. Skipping.');
359
+ return;
360
+ }
361
+
362
+ log.info('⚙️ Creating sessions table...');
363
+
364
+ if (DB.driver === 'mysql') {
365
+ await DB.raw(`
366
+ CREATE TABLE sessions (
367
+ sid VARCHAR(255) PRIMARY KEY,
368
+ data TEXT NOT NULL,
369
+ expires BIGINT NOT NULL
370
+ )
371
+ `);
372
+
373
+ await DB.raw(`
374
+ CREATE INDEX idx_sessions_expires ON sessions (expires)
375
+ `);
376
+ }
377
+
378
+ if (DB.driver === 'sqlite') {
379
+ await DB.raw(`
380
+ CREATE TABLE sessions (
381
+ sid TEXT PRIMARY KEY,
382
+ data TEXT NOT NULL,
383
+ expires INTEGER NOT NULL
384
+ )
385
+ `);
386
+
387
+ await DB.raw(`
388
+ CREATE INDEX idx_sessions_expires ON sessions (expires)
389
+ `);
390
+ }
391
+
392
+ if (DB.driver === 'pg') {
393
+ await DB.raw(`
394
+ CREATE TABLE sessions (
395
+ sid VARCHAR(255) PRIMARY KEY,
396
+ data TEXT NOT NULL,
397
+ expires BIGINT NOT NULL
398
+ )
399
+ `);
400
+
401
+ await DB.raw(`
402
+ CREATE INDEX idx_sessions_expires ON sessions (expires)
403
+ `);
404
+ }
405
+
406
+ log.success('✅ sessions table created successfully.');
407
+ }
408
+
409
+
351
410
 
352
411
  async function rollbackLastMigration() {
353
412
  DB.initFromEnv();
package/lib/index.d.ts CHANGED
@@ -488,6 +488,31 @@ export class DBError extends Error {
488
488
  constructor(message: any, meta?: {});
489
489
  meta: {};
490
490
  }
491
+ export class LamixSessionStore {
492
+ constructor(options?: {});
493
+ ttl: any;
494
+ cleanupInterval: any;
495
+ logger: any;
496
+ compress: any;
497
+ cache: LRU<any, any>;
498
+ redisEnabled: boolean;
499
+ redisRetryAt: number;
500
+ redisCooldown: any;
501
+ redis: any;
502
+ _safeCb(cb: any, err: any, result?: any): void;
503
+ _hasSessionModel(): boolean;
504
+ _canUseRedis(): boolean;
505
+ _markRedisFailure(err: any): void;
506
+ _serialize(data: any): string;
507
+ _deserialize(raw: any): any;
508
+ _checkRedisHealth(): Promise<void>;
509
+ get(sid: any, cb: any): Promise<void>;
510
+ set(sid: any, sessionData: any, cb: any): Promise<void>;
511
+ destroy(sid: any, cb: any): Promise<void>;
512
+ touch(sid: any, sessionData: any, cb: any): Promise<void>;
513
+ _startCleanup(): void;
514
+ _cleanupTimer: any;
515
+ }
491
516
  export class BaseModel extends Model {
492
517
  static passwordField: string;
493
518
  static hashRounds: number;
@@ -586,4 +611,5 @@ declare class Relation {
586
611
  deleteBehavior: any;
587
612
  onDelete(behavior: any): this;
588
613
  }
614
+ import LRU = require("lru-cache");
589
615
  export {};
package/lib/index.js CHANGED
@@ -135,31 +135,6 @@ class DB {
135
135
  }
136
136
 
137
137
  /* ---------- Driver ---------- */
138
-
139
- // static _ensureModule() {
140
- // if (!this.driver) this.initFromEnv();
141
-
142
- // if (this.driver === 'mysql') {
143
- // const m = tryRequire('mysql2/promise');
144
- // if (!m) throw new DBError('Missing mysql2');
145
- // return m;
146
- // }
147
-
148
- // if (this.driver === 'pg') {
149
- // const m = tryRequire('pg');
150
- // if (!m) throw new DBError('Missing pg');
151
- // return m;
152
- // }
153
-
154
- // if (this.driver === 'sqlite') {
155
- // const m = tryRequire('sqlite3');
156
- // if (!m) throw new DBError('Missing sqlite3');
157
- // return m;
158
- // }
159
-
160
- // throw new DBError(`Unsupported driver: ${this.driver}`);
161
- // }
162
-
163
138
  static _ensureModule() {
164
139
  if (!this.driver) this.initFromEnv();
165
140
 
@@ -3916,6 +3891,12 @@ class Model {
3916
3891
  }
3917
3892
  }
3918
3893
 
3894
+ const session = require('express-session');
3895
+ const Redis = require('ioredis');
3896
+ const LRU = require('lru-cache');
3897
+ const zlib = require('zlib');
3898
+
3899
+ /* -------------------- DB Model -------------------- */
3919
3900
  class Session extends Model {
3920
3901
  static table = 'sessions';
3921
3902
  static primaryKey = 'sid';
@@ -3925,44 +3906,159 @@ class Session extends Model {
3925
3906
  static fillable = ['sid', 'data', 'expires'];
3926
3907
  }
3927
3908
 
3928
- const session = require('express-session');
3909
+ /* -------------------- Optional Session Model -------------------- */
3910
+ let SessionModel = null;
3911
+
3912
+ try {
3913
+ // If this throws, DB-backed sessions are simply disabled
3914
+ SessionModel = Session;
3915
+ } catch (err) {
3916
+ SessionModel = null;
3917
+ }
3929
3918
 
3919
+ /* -------------------- Store -------------------- */
3930
3920
  class LamixSessionStore extends session.Store {
3931
3921
  constructor(options = {}) {
3932
3922
  super();
3933
3923
 
3934
- this.ttl = options.ttl || 86400; // seconds
3924
+ this.ttl = options.ttl || 86400;
3935
3925
  this.cleanupInterval = options.cleanupInterval || 60000;
3926
+ this.logger = options.logger || console;
3927
+ this.compress = options.compress ?? true;
3928
+
3929
+ /* -------- In-memory cache -------- */
3930
+ this.cache = new LRU({
3931
+ max: options.cacheSize || 1000,
3932
+ ttl: options.cacheTTL || 60000,
3933
+ });
3934
+
3935
+ /* -------- Redis -------- */
3936
+ this.redisEnabled = false;
3937
+ this.redisRetryAt = 0;
3938
+ this.redisCooldown = options.redisCooldown || 30000;
3939
+
3940
+ if (options.redis) {
3941
+ this.redis =
3942
+ options.redis instanceof Redis
3943
+ ? options.redis
3944
+ : new Redis(options.redis);
3945
+
3946
+ this._checkRedisHealth();
3947
+ }
3936
3948
 
3937
3949
  this._startCleanup();
3938
3950
  }
3939
3951
 
3940
- /* ---------- Get ---------- */
3952
+ /* -------------------- Safety Helpers -------------------- */
3953
+ _safeCb(cb, err, result = null) {
3954
+ try {
3955
+ cb(err, result);
3956
+ } catch (e) {
3957
+ this.logger.error('Session callback threw', e);
3958
+ }
3959
+ }
3941
3960
 
3942
- async get(sid, cb) {
3961
+ _hasSessionModel() {
3962
+ return !!SessionModel;
3963
+ }
3964
+
3965
+ _canUseRedis() {
3966
+ return this.redisEnabled && Date.now() > this.redisRetryAt;
3967
+ }
3968
+
3969
+ _markRedisFailure(err) {
3970
+ this.redisEnabled = false;
3971
+ this.redisRetryAt = Date.now() + this.redisCooldown;
3972
+ this.logger.warn('Redis disabled temporarily', err);
3973
+ }
3974
+
3975
+ /* -------------------- Serialization -------------------- */
3976
+ _serialize(data) {
3943
3977
  try {
3944
- const now = Date.now();
3978
+ const json = JSON.stringify(data);
3979
+ return this.compress
3980
+ ? zlib.deflateSync(Buffer.from(json)).toString('base64')
3981
+ : json;
3982
+ } catch (err) {
3983
+ this.logger.warn('Serialize failed, skipping persistence', err);
3984
+ return null;
3985
+ }
3986
+ }
3945
3987
 
3946
- const row = await Session
3947
- .query()
3948
- .where('sid', sid)
3949
- .where('expires', '>', now)
3950
- .first();
3988
+ _deserialize(raw) {
3989
+ if (!raw) return null;
3951
3990
 
3952
- if (!row) return cb(null, null);
3991
+ try {
3992
+ const str = this.compress
3993
+ ? zlib.inflateSync(Buffer.from(raw, 'base64')).toString()
3994
+ : raw;
3953
3995
 
3954
- cb(null, JSON.parse(row.data));
3996
+ return JSON.parse(str);
3955
3997
  } catch (err) {
3956
- cb(new DBError('Failed to load session', {
3957
- sid,
3958
- operation: 'get',
3959
- err
3960
- }));
3998
+ this.logger.warn('Deserialize failed, dropping session', err);
3999
+ return null;
3961
4000
  }
3962
4001
  }
3963
4002
 
3964
- /* ---------- Set ---------- */
4003
+ /* -------------------- Redis Health -------------------- */
4004
+ async _checkRedisHealth() {
4005
+ try {
4006
+ await this.redis.ping();
4007
+ this.redisEnabled = true;
4008
+ this.logger.info('Redis session store active');
4009
+
4010
+ this.redis.on('error', (err) => this._markRedisFailure(err));
4011
+ this.redis.on('connect', () => {
4012
+ this.redisEnabled = true;
4013
+ this.logger.info('Redis reconnected');
4014
+ });
4015
+ } catch (err) {
4016
+ this.redisEnabled = false;
4017
+ this.logger.warn('Redis unavailable', err);
4018
+ }
4019
+ }
4020
+
4021
+ /* -------------------- GET -------------------- */
4022
+ async get(sid, cb) {
4023
+ let sessionData = null;
4024
+
4025
+ try {
4026
+ if (this.cache.has(sid)) {
4027
+ return this._safeCb(cb, null, this.cache.get(sid));
4028
+ }
4029
+
4030
+ if (this._canUseRedis()) {
4031
+ try {
4032
+ const raw = await this.redis.get(`sess:${sid}`);
4033
+ sessionData = this._deserialize(raw);
4034
+ } catch (err) {
4035
+ this._markRedisFailure(err);
4036
+ }
4037
+ }
4038
+
4039
+ if (!sessionData && this._hasSessionModel()) {
4040
+ try {
4041
+ const row = await SessionModel.query()
4042
+ .where('sid', sid)
4043
+ .where('expires', '>', Date.now())
4044
+ .first();
4045
+
4046
+ if (row) sessionData = this._deserialize(row.data);
4047
+ } catch (err) {
4048
+ this.logger.warn('DB GET failed, disabling DB sessions', err);
4049
+ SessionModel = null;
4050
+ }
4051
+ }
4052
+
4053
+ if (sessionData) this.cache.set(sid, sessionData);
4054
+ this._safeCb(cb, null, sessionData);
4055
+ } catch (err) {
4056
+ this.logger.error('GET failed safely', err);
4057
+ this._safeCb(cb, null, null);
4058
+ }
4059
+ }
3965
4060
 
4061
+ /* -------------------- SET -------------------- */
3966
4062
  async set(sid, sessionData, cb) {
3967
4063
  try {
3968
4064
  const expires =
@@ -3970,58 +4066,75 @@ class LamixSessionStore extends session.Store {
3970
4066
  ? new Date(sessionData.cookie.expires).getTime()
3971
4067
  : Date.now() + this.ttl * 1000;
3972
4068
 
3973
- const payload = {
3974
- sid,
3975
- data: JSON.stringify(sessionData),
3976
- expires
3977
- };
4069
+ const data = this._serialize(sessionData);
4070
+ if (!data) return this._safeCb(cb, null);
3978
4071
 
3979
- // const existing = await Session.find(sid);
3980
- const existing = await Session
3981
- .query()
3982
- .where('sid', sid)
3983
- .first();
4072
+ this.cache.set(sid, sessionData);
3984
4073
 
3985
- if (existing) {
3986
- await existing.update(payload);
3987
- } else {
3988
- const session = new Session(payload, false);
3989
- await session.saveNew(payload);
4074
+ if (this._hasSessionModel()) {
4075
+ try {
4076
+ const existing = await SessionModel.query()
4077
+ .where('sid', sid)
4078
+ .first();
4079
+
4080
+ if (existing) {
4081
+ await existing.update({ data, expires });
4082
+ } else {
4083
+ await new SessionModel({ sid, data, expires }, false).saveNew();
4084
+ }
4085
+ } catch (err) {
4086
+ this.logger.warn('DB SET failed, disabling DB sessions', err);
4087
+ SessionModel = null;
4088
+ }
3990
4089
  }
3991
4090
 
3992
- cb(null);
4091
+ if (this._canUseRedis()) {
4092
+ try {
4093
+ await this.redis.set(`sess:${sid}`, data, 'PX', expires - Date.now());
4094
+ } catch (err) {
4095
+ this._markRedisFailure(err);
4096
+ }
4097
+ }
4098
+
4099
+ this._safeCb(cb, null);
3993
4100
  } catch (err) {
3994
- cb(new DBError('Failed to persist session', {
3995
- sid,
3996
- operation: 'set',
3997
- err
3998
- }));
4101
+ this.logger.error('SET failed safely', err);
4102
+ this._safeCb(cb, null);
3999
4103
  }
4000
4104
  }
4001
4105
 
4002
- /* ---------- Destroy ---------- */
4003
-
4106
+ /* -------------------- DESTROY -------------------- */
4004
4107
  async destroy(sid, cb) {
4005
4108
  try {
4006
- await Session
4007
- .query()
4008
- .where('sid', sid)
4009
- .delete();
4109
+ this.cache.delete(sid);
4110
+
4111
+ if (this._hasSessionModel()) {
4112
+ try {
4113
+ await SessionModel.query().where('sid', sid).delete();
4114
+ } catch (err) {
4115
+ this.logger.warn('DB DESTROY failed, disabling DB sessions', err);
4116
+ SessionModel = null;
4117
+ }
4118
+ }
4010
4119
 
4011
- cb(null);
4120
+ if (this._canUseRedis()) {
4121
+ try {
4122
+ await this.redis.del(`sess:${sid}`);
4123
+ } catch (err) {
4124
+ this._markRedisFailure(err);
4125
+ }
4126
+ }
4127
+
4128
+ this._safeCb(cb, null);
4012
4129
  } catch (err) {
4013
- cb(new DBError('Failed to destroy session', {
4014
- sid,
4015
- operation: 'destroy',
4016
- err
4017
- }));
4130
+ this.logger.error('DESTROY failed safely', err);
4131
+ this._safeCb(cb, null);
4018
4132
  }
4019
4133
  }
4020
4134
 
4021
- /* ---------- Touch ---------- */
4022
-
4135
+ /* -------------------- TOUCH -------------------- */
4023
4136
  async touch(sid, sessionData, cb) {
4024
- if (!sessionData) return cb(null);
4137
+ if (!sessionData) return this._safeCb(cb, null);
4025
4138
 
4026
4139
  try {
4027
4140
  const expires =
@@ -4029,45 +4142,54 @@ class LamixSessionStore extends session.Store {
4029
4142
  ? new Date(sessionData.cookie.expires).getTime()
4030
4143
  : Date.now() + this.ttl * 1000;
4031
4144
 
4032
- await Session
4033
- .query()
4034
- .where('sid', sid)
4035
- .update({ expires });
4145
+ this.cache.set(sid, sessionData);
4146
+
4147
+ if (this._hasSessionModel()) {
4148
+ try {
4149
+ await SessionModel.query().where('sid', sid).update({ expires });
4150
+ } catch (err) {
4151
+ this.logger.warn('DB TOUCH failed, disabling DB sessions', err);
4152
+ SessionModel = null;
4153
+ }
4154
+ }
4036
4155
 
4037
- cb();
4156
+ if (this._canUseRedis()) {
4157
+ try {
4158
+ await this.redis.pexpire(`sess:${sid}`, expires - Date.now());
4159
+ } catch (err) {
4160
+ this._markRedisFailure(err);
4161
+ }
4162
+ }
4163
+
4164
+ this._safeCb(cb, null);
4038
4165
  } catch (err) {
4039
- cb(new DBError('Failed to touch session', {
4040
- sid,
4041
- operation: 'touch',
4042
- err
4043
- }));
4166
+ this.logger.error('TOUCH failed safely', err);
4167
+ this._safeCb(cb, null);
4044
4168
  }
4045
4169
  }
4046
4170
 
4047
- /* ---------- Cleanup ---------- */
4048
-
4171
+ /* -------------------- Cleanup -------------------- */
4049
4172
  _startCleanup() {
4173
+ if (!this._hasSessionModel()) return;
4174
+
4050
4175
  this._cleanupTimer = setInterval(async () => {
4051
4176
  try {
4052
- await Session
4053
- .query()
4177
+ await SessionModel.query()
4054
4178
  .where('expires', '<', Date.now())
4055
4179
  .delete();
4056
4180
  } catch (err) {
4057
- // cleanup must NEVER fail silently
4058
- throw new DBError('Session cleanup failed', {
4059
- operation: 'cleanup',
4060
- err
4061
- });
4181
+ this.logger.warn('Cleanup failed, disabling DB sessions', err);
4182
+ SessionModel = null;
4183
+ clearInterval(this._cleanupTimer);
4062
4184
  }
4063
4185
  }, this.cleanupInterval);
4064
4186
 
4065
4187
  this._cleanupTimer.unref();
4066
4188
  }
4067
-
4068
4189
  }
4069
4190
 
4070
4191
 
4192
+
4071
4193
  // --- BaseModel with bcrypt hashing ---
4072
4194
  const bcrypt = tryRequire('bcrypt');
4073
4195
  class BaseModel extends Model {
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "lamix",
3
- "version": "4.2.13",
3
+ "version": "4.2.15",
4
4
  "description": "lamix - ORM for Node-express js",
5
5
  "main": "./lib",
6
+ "type": "commonjs",
6
7
  "exports": {
7
8
  ".": {
8
9
  "require": "./lib/index.js",
@@ -31,6 +32,9 @@
31
32
  "dependencies": {
32
33
  "bcrypt": "^6.0.0",
33
34
  "express-session": "^1.19.0",
35
+ "lru-cache": "^7.18.3",
36
+ "zlib": "^1.0.5",
37
+ "ioredis": "^5.9.2",
34
38
  "chalk": "^4.1.2",
35
39
  "dotenv": "^17.2.2"
36
40
  },
@@ -39,6 +43,7 @@
39
43
  "orm",
40
44
  "nodejs",
41
45
  "database",
46
+ "express",
42
47
  "sql"
43
48
  ],
44
49
  "author": {