lamix 4.2.12 → 4.2.14
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 +45 -4
- package/bin/cli.js +60 -1
- package/lib/index.d.ts +11 -0
- package/lib/index.js +171 -11
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -87,7 +87,7 @@ npm i lamix
|
|
|
87
87
|
2. Install dependencies (if any).
|
|
88
88
|
> This tool uses `mysql2`,`pg`,`sqlite3` driver, so make sure you install your prefered driver:
|
|
89
89
|
DATABASE CONNECTION IN `.env`FILE
|
|
90
|
-
Configure DB via environment variables: `'DB_CONNECTION=mysql',DB_HOST=your db hast`, `DB_USER=your db user`, `DB_PASS=your db password`, `DB_NAME=your db name`, `DB_PORT=your db port`, `DB_CONNECTION_LIMIT=
|
|
90
|
+
Configure DB via environment variables: `'DB_CONNECTION=mysql',DB_HOST=your db hast`, `DB_USER=your db user`, `DB_PASS=your db password`, `DB_NAME=your db name`, `DB_PORT=your db port`, `DB_CONNECTION_LIMIT=10`.
|
|
91
91
|
|
|
92
92
|
## Quick start
|
|
93
93
|
for database connection use any driver of your choice eg
|
|
@@ -271,7 +271,7 @@ class User extends BaseModel {
|
|
|
271
271
|
}
|
|
272
272
|
|
|
273
273
|
|
|
274
|
-
|
|
274
|
+
# Many-to-many: User ↔ Role via pivot user_roles (user_id, role_id)
|
|
275
275
|
roles() {
|
|
276
276
|
const Role = require('./Role');
|
|
277
277
|
return this.belongsToMany(
|
|
@@ -282,9 +282,50 @@ class User extends BaseModel {
|
|
|
282
282
|
).onDelete('detach');
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
-
|
|
285
|
+
# One-to-many: User -> Post
|
|
286
286
|
posts() {
|
|
287
287
|
const Post = require('./Post');
|
|
288
288
|
return this.hasMany(Post', 'user_id', 'id').onDelete('cascade');
|
|
289
289
|
}
|
|
290
|
-
|
|
290
|
+
|
|
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.
|
|
294
|
+
|
|
295
|
+
# Session setup
|
|
296
|
+
const express = require('express');
|
|
297
|
+
const session = require('express-session');
|
|
298
|
+
const { DB, LamixSessionStore } = require('lamix');
|
|
299
|
+
|
|
300
|
+
DB.initFromEnv();
|
|
301
|
+
|
|
302
|
+
(async () => {
|
|
303
|
+
await DB.connect();
|
|
304
|
+
})();
|
|
305
|
+
|
|
306
|
+
const app = express();
|
|
307
|
+
|
|
308
|
+
app.use(
|
|
309
|
+
session({
|
|
310
|
+
name: 'lamix.sid',
|
|
311
|
+
secret: process.env.SESSION_SECRET || 'dev-secret',
|
|
312
|
+
resave: false,
|
|
313
|
+
saveUninitialized: false,
|
|
314
|
+
store: new LamixSessionStore({
|
|
315
|
+
ttl: 60 * 60 * 24, // 1 day
|
|
316
|
+
}),
|
|
317
|
+
cookie: {
|
|
318
|
+
httpOnly: true,
|
|
319
|
+
secure: false, // true behind HTTPS
|
|
320
|
+
maxAge: 1000 * 60 * 60 * 24,
|
|
321
|
+
},
|
|
322
|
+
})
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
app.get('/', (req, res) => {
|
|
326
|
+
req.session.views = (req.session.views || 0) + 1;
|
|
327
|
+
res.json({ views: req.session.views });
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
app.listen(3000);
|
|
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,17 @@ 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
|
+
get(sid: any, cb: any): Promise<any>;
|
|
496
|
+
set(sid: any, sessionData: any, cb: any): Promise<void>;
|
|
497
|
+
destroy(sid: any, cb: any): Promise<void>;
|
|
498
|
+
touch(sid: any, sessionData: any, cb: any): Promise<any>;
|
|
499
|
+
_startCleanup(): void;
|
|
500
|
+
_cleanupTimer: NodeJS.Timeout;
|
|
501
|
+
}
|
|
491
502
|
export class BaseModel extends Model {
|
|
492
503
|
static passwordField: string;
|
|
493
504
|
static hashRounds: number;
|
package/lib/index.js
CHANGED
|
@@ -135,31 +135,39 @@ class DB {
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
/* ---------- Driver ---------- */
|
|
138
|
-
|
|
139
138
|
static _ensureModule() {
|
|
140
139
|
if (!this.driver) this.initFromEnv();
|
|
141
140
|
|
|
141
|
+
const safeRequire = (name) => {
|
|
142
|
+
try {
|
|
143
|
+
return require(name);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
if (err.code === 'ERR_REQUIRE_ASYNC_MODULE') {
|
|
146
|
+
throw new DBError(
|
|
147
|
+
`${name} is ESM-only or uses top-level await. Use a CommonJS version.`,
|
|
148
|
+
{ module: name, err }
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
throw err;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
142
155
|
if (this.driver === 'mysql') {
|
|
143
|
-
|
|
144
|
-
if (!m) throw new DBError('Missing mysql2');
|
|
145
|
-
return m;
|
|
156
|
+
return safeRequire('mysql2/promise');
|
|
146
157
|
}
|
|
147
158
|
|
|
148
159
|
if (this.driver === 'pg') {
|
|
149
|
-
|
|
150
|
-
if (!m) throw new DBError('Missing pg');
|
|
151
|
-
return m;
|
|
160
|
+
return safeRequire('pg');
|
|
152
161
|
}
|
|
153
162
|
|
|
154
163
|
if (this.driver === 'sqlite') {
|
|
155
|
-
|
|
156
|
-
if (!m) throw new DBError('Missing sqlite3');
|
|
157
|
-
return m;
|
|
164
|
+
return safeRequire('sqlite3');
|
|
158
165
|
}
|
|
159
166
|
|
|
160
167
|
throw new DBError(`Unsupported driver: ${this.driver}`);
|
|
161
168
|
}
|
|
162
169
|
|
|
170
|
+
|
|
163
171
|
/* ---------- Connection ---------- */
|
|
164
172
|
|
|
165
173
|
static async connect() {
|
|
@@ -3883,6 +3891,158 @@ class Model {
|
|
|
3883
3891
|
}
|
|
3884
3892
|
}
|
|
3885
3893
|
|
|
3894
|
+
class Session extends Model {
|
|
3895
|
+
static table = 'sessions';
|
|
3896
|
+
static primaryKey = 'sid';
|
|
3897
|
+
static slugKey = null;
|
|
3898
|
+
static timestamps = false;
|
|
3899
|
+
|
|
3900
|
+
static fillable = ['sid', 'data', 'expires'];
|
|
3901
|
+
}
|
|
3902
|
+
|
|
3903
|
+
const session = require('express-session');
|
|
3904
|
+
|
|
3905
|
+
class LamixSessionStore extends session.Store {
|
|
3906
|
+
constructor(options = {}) {
|
|
3907
|
+
super();
|
|
3908
|
+
|
|
3909
|
+
this.ttl = options.ttl || 86400; // seconds
|
|
3910
|
+
this.cleanupInterval = options.cleanupInterval || 60000;
|
|
3911
|
+
|
|
3912
|
+
this._startCleanup();
|
|
3913
|
+
}
|
|
3914
|
+
|
|
3915
|
+
/* ---------- Get ---------- */
|
|
3916
|
+
|
|
3917
|
+
async get(sid, cb) {
|
|
3918
|
+
try {
|
|
3919
|
+
const now = Date.now();
|
|
3920
|
+
|
|
3921
|
+
const row = await Session
|
|
3922
|
+
.query()
|
|
3923
|
+
.where('sid', sid)
|
|
3924
|
+
.where('expires', '>', now)
|
|
3925
|
+
.first();
|
|
3926
|
+
|
|
3927
|
+
if (!row) return cb(null, null);
|
|
3928
|
+
|
|
3929
|
+
cb(null, JSON.parse(row.data));
|
|
3930
|
+
} catch (err) {
|
|
3931
|
+
cb(new DBError('Failed to load session', {
|
|
3932
|
+
sid,
|
|
3933
|
+
operation: 'get',
|
|
3934
|
+
err
|
|
3935
|
+
}));
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
|
|
3939
|
+
/* ---------- Set ---------- */
|
|
3940
|
+
|
|
3941
|
+
async set(sid, sessionData, cb) {
|
|
3942
|
+
try {
|
|
3943
|
+
const expires =
|
|
3944
|
+
sessionData.cookie?.expires
|
|
3945
|
+
? new Date(sessionData.cookie.expires).getTime()
|
|
3946
|
+
: Date.now() + this.ttl * 1000;
|
|
3947
|
+
|
|
3948
|
+
const payload = {
|
|
3949
|
+
sid,
|
|
3950
|
+
data: JSON.stringify(sessionData),
|
|
3951
|
+
expires
|
|
3952
|
+
};
|
|
3953
|
+
|
|
3954
|
+
// const existing = await Session.find(sid);
|
|
3955
|
+
const existing = await Session
|
|
3956
|
+
.query()
|
|
3957
|
+
.where('sid', sid)
|
|
3958
|
+
.first();
|
|
3959
|
+
|
|
3960
|
+
if (existing) {
|
|
3961
|
+
await existing.update(payload);
|
|
3962
|
+
} else {
|
|
3963
|
+
const session = new Session(payload, false);
|
|
3964
|
+
await session.saveNew(payload);
|
|
3965
|
+
}
|
|
3966
|
+
|
|
3967
|
+
cb(null);
|
|
3968
|
+
} catch (err) {
|
|
3969
|
+
cb(new DBError('Failed to persist session', {
|
|
3970
|
+
sid,
|
|
3971
|
+
operation: 'set',
|
|
3972
|
+
err
|
|
3973
|
+
}));
|
|
3974
|
+
}
|
|
3975
|
+
}
|
|
3976
|
+
|
|
3977
|
+
/* ---------- Destroy ---------- */
|
|
3978
|
+
|
|
3979
|
+
async destroy(sid, cb) {
|
|
3980
|
+
try {
|
|
3981
|
+
await Session
|
|
3982
|
+
.query()
|
|
3983
|
+
.where('sid', sid)
|
|
3984
|
+
.delete();
|
|
3985
|
+
|
|
3986
|
+
cb(null);
|
|
3987
|
+
} catch (err) {
|
|
3988
|
+
cb(new DBError('Failed to destroy session', {
|
|
3989
|
+
sid,
|
|
3990
|
+
operation: 'destroy',
|
|
3991
|
+
err
|
|
3992
|
+
}));
|
|
3993
|
+
}
|
|
3994
|
+
}
|
|
3995
|
+
|
|
3996
|
+
/* ---------- Touch ---------- */
|
|
3997
|
+
|
|
3998
|
+
async touch(sid, sessionData, cb) {
|
|
3999
|
+
if (!sessionData) return cb(null);
|
|
4000
|
+
|
|
4001
|
+
try {
|
|
4002
|
+
const expires =
|
|
4003
|
+
sessionData.cookie?.expires
|
|
4004
|
+
? new Date(sessionData.cookie.expires).getTime()
|
|
4005
|
+
: Date.now() + this.ttl * 1000;
|
|
4006
|
+
|
|
4007
|
+
await Session
|
|
4008
|
+
.query()
|
|
4009
|
+
.where('sid', sid)
|
|
4010
|
+
.update({ expires });
|
|
4011
|
+
|
|
4012
|
+
cb();
|
|
4013
|
+
} catch (err) {
|
|
4014
|
+
cb(new DBError('Failed to touch session', {
|
|
4015
|
+
sid,
|
|
4016
|
+
operation: 'touch',
|
|
4017
|
+
err
|
|
4018
|
+
}));
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
|
|
4022
|
+
/* ---------- Cleanup ---------- */
|
|
4023
|
+
|
|
4024
|
+
_startCleanup() {
|
|
4025
|
+
this._cleanupTimer = setInterval(async () => {
|
|
4026
|
+
try {
|
|
4027
|
+
await Session
|
|
4028
|
+
.query()
|
|
4029
|
+
.where('expires', '<', Date.now())
|
|
4030
|
+
.delete();
|
|
4031
|
+
} catch (err) {
|
|
4032
|
+
// cleanup must NEVER fail silently
|
|
4033
|
+
throw new DBError('Session cleanup failed', {
|
|
4034
|
+
operation: 'cleanup',
|
|
4035
|
+
err
|
|
4036
|
+
});
|
|
4037
|
+
}
|
|
4038
|
+
}, this.cleanupInterval);
|
|
4039
|
+
|
|
4040
|
+
this._cleanupTimer.unref();
|
|
4041
|
+
}
|
|
4042
|
+
|
|
4043
|
+
}
|
|
4044
|
+
|
|
4045
|
+
|
|
3886
4046
|
// --- BaseModel with bcrypt hashing ---
|
|
3887
4047
|
const bcrypt = tryRequire('bcrypt');
|
|
3888
4048
|
class BaseModel extends Model {
|
|
@@ -3971,4 +4131,4 @@ class BaseModel extends Model {
|
|
|
3971
4131
|
}
|
|
3972
4132
|
}
|
|
3973
4133
|
|
|
3974
|
-
module.exports = { DB, Model, Validator, ValidationError, Collection, QueryBuilder, HasMany, HasOne, BelongsTo, BelongsToMany, DBError, BaseModel};
|
|
4134
|
+
module.exports = { DB, Model, Validator, ValidationError, Collection, QueryBuilder, HasMany, HasOne, BelongsTo, BelongsToMany, DBError, LamixSessionStore, BaseModel};
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lamix",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.14",
|
|
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",
|
|
@@ -30,6 +31,7 @@
|
|
|
30
31
|
},
|
|
31
32
|
"dependencies": {
|
|
32
33
|
"bcrypt": "^6.0.0",
|
|
34
|
+
"express-session": "^1.19.0",
|
|
33
35
|
"chalk": "^4.1.2",
|
|
34
36
|
"dotenv": "^17.2.2"
|
|
35
37
|
},
|
|
@@ -38,6 +40,7 @@
|
|
|
38
40
|
"orm",
|
|
39
41
|
"nodejs",
|
|
40
42
|
"database",
|
|
43
|
+
"express",
|
|
41
44
|
"sql"
|
|
42
45
|
],
|
|
43
46
|
"author": {
|
|
@@ -46,6 +49,6 @@
|
|
|
46
49
|
},
|
|
47
50
|
"license": "MIT",
|
|
48
51
|
"engines": {
|
|
49
|
-
"node": ">=
|
|
52
|
+
"node": ">=18.0.0"
|
|
50
53
|
}
|
|
51
54
|
}
|