lamix 4.2.11 → 4.2.13
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 +56 -3
- package/lib/index.js +584 -158
- package/package.json +4 -4
- package/examples/Alternative.md +0 -23
- package/examples/CRUD.md +0 -51
- package/examples/Post.md +0 -28
- package/examples/PostController.md +0 -93
- package/examples/Query Builder.md +0 -55
- package/examples/Relations.md +0 -16
- package/examples/Role.md +0 -26
- package/examples/RoleController.md +0 -65
- package/examples/Usages.md +0 -132
- package/examples/User.md +0 -42
- package/examples/UserController.md +0 -98
package/lib/index.js
CHANGED
|
@@ -136,30 +136,63 @@ class DB {
|
|
|
136
136
|
|
|
137
137
|
/* ---------- Driver ---------- */
|
|
138
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
|
+
|
|
139
163
|
static _ensureModule() {
|
|
140
164
|
if (!this.driver) this.initFromEnv();
|
|
141
165
|
|
|
166
|
+
const safeRequire = (name) => {
|
|
167
|
+
try {
|
|
168
|
+
return require(name);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
if (err.code === 'ERR_REQUIRE_ASYNC_MODULE') {
|
|
171
|
+
throw new DBError(
|
|
172
|
+
`${name} is ESM-only or uses top-level await. Use a CommonJS version.`,
|
|
173
|
+
{ module: name, err }
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
throw err;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
142
180
|
if (this.driver === 'mysql') {
|
|
143
|
-
|
|
144
|
-
if (!m) throw new DBError('Missing mysql2');
|
|
145
|
-
return m;
|
|
181
|
+
return safeRequire('mysql2/promise');
|
|
146
182
|
}
|
|
147
183
|
|
|
148
184
|
if (this.driver === 'pg') {
|
|
149
|
-
|
|
150
|
-
if (!m) throw new DBError('Missing pg');
|
|
151
|
-
return m;
|
|
185
|
+
return safeRequire('pg');
|
|
152
186
|
}
|
|
153
187
|
|
|
154
188
|
if (this.driver === 'sqlite') {
|
|
155
|
-
|
|
156
|
-
if (!m) throw new DBError('Missing sqlite3');
|
|
157
|
-
return m;
|
|
189
|
+
return safeRequire('sqlite3');
|
|
158
190
|
}
|
|
159
191
|
|
|
160
192
|
throw new DBError(`Unsupported driver: ${this.driver}`);
|
|
161
193
|
}
|
|
162
194
|
|
|
195
|
+
|
|
163
196
|
/* ---------- Connection ---------- */
|
|
164
197
|
|
|
165
198
|
static async connect() {
|
|
@@ -232,31 +265,51 @@ class DB {
|
|
|
232
265
|
|
|
233
266
|
try {
|
|
234
267
|
const result = await executor();
|
|
268
|
+
|
|
235
269
|
this._emit('query', {
|
|
236
270
|
sql,
|
|
237
271
|
bindings,
|
|
238
272
|
time: performance.now() - start,
|
|
239
273
|
driver: this.driver
|
|
240
274
|
});
|
|
275
|
+
|
|
241
276
|
return result;
|
|
242
277
|
} catch (err) {
|
|
243
|
-
|
|
278
|
+
const isConnectionError =
|
|
279
|
+
err?.code === 'PROTOCOL_CONNECTION_LOST' ||
|
|
280
|
+
err?.code === 'ECONNRESET' ||
|
|
281
|
+
err?.code === 'ECONNREFUSED' ||
|
|
282
|
+
err?.code === 'ETIMEDOUT' ||
|
|
283
|
+
/dead|lost|timeout|reset|closed|connect/i.test(err.message);
|
|
284
|
+
|
|
285
|
+
this._emit('error', {
|
|
286
|
+
err,
|
|
287
|
+
sql,
|
|
288
|
+
bindings,
|
|
289
|
+
attempt,
|
|
290
|
+
connectionError: isConnectionError
|
|
291
|
+
});
|
|
244
292
|
|
|
245
293
|
const canRetry =
|
|
246
|
-
!isWrite ||
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (
|
|
250
|
-
canRetry &&
|
|
251
|
-
attempt < this.retryAttempts &&
|
|
252
|
-
/dead|lost|timeout|reset|closed/i.test(err.message)
|
|
253
|
-
) {
|
|
294
|
+
(!isWrite || this._als.getStore()?.inTransaction) &&
|
|
295
|
+
isConnectionError;
|
|
296
|
+
|
|
297
|
+
if (canRetry && attempt < this.retryAttempts) {
|
|
254
298
|
await this.reconnect();
|
|
255
299
|
await sleep(100 * attempt);
|
|
256
300
|
continue;
|
|
257
301
|
}
|
|
258
302
|
|
|
259
|
-
throw new DBError(
|
|
303
|
+
throw new DBError(
|
|
304
|
+
isConnectionError ? 'DB connection failed' : 'DB query failed',
|
|
305
|
+
{
|
|
306
|
+
sql,
|
|
307
|
+
bindings,
|
|
308
|
+
attempt,
|
|
309
|
+
connectionError: isConnectionError,
|
|
310
|
+
err
|
|
311
|
+
}
|
|
312
|
+
);
|
|
260
313
|
}
|
|
261
314
|
}
|
|
262
315
|
}
|
|
@@ -1146,9 +1199,13 @@ class QueryBuilder {
|
|
|
1146
1199
|
|
|
1147
1200
|
// Helper to normalize operator
|
|
1148
1201
|
_normalizeOperator(operator) {
|
|
1149
|
-
const op = operator ? operator.toUpperCase() : '='
|
|
1202
|
+
const op = operator ? operator.toUpperCase() : '='
|
|
1150
1203
|
if (!VALID_OPERATORS.includes(op)) {
|
|
1151
|
-
throw new
|
|
1204
|
+
throw new DBError('Invalid SQL operator', {
|
|
1205
|
+
operator,
|
|
1206
|
+
normalized: op,
|
|
1207
|
+
method: '_normalizeOperator'
|
|
1208
|
+
});
|
|
1152
1209
|
}
|
|
1153
1210
|
|
|
1154
1211
|
// Convert ILIKE to proper operator for MySQL
|
|
@@ -1348,7 +1405,13 @@ class QueryBuilder {
|
|
|
1348
1405
|
}
|
|
1349
1406
|
|
|
1350
1407
|
whereIn(column, values = []) {
|
|
1351
|
-
if (!Array.isArray(values))
|
|
1408
|
+
if (!Array.isArray(values)) {
|
|
1409
|
+
throw new DBError('whereIn expects an array', {
|
|
1410
|
+
method: 'whereIn',
|
|
1411
|
+
column,
|
|
1412
|
+
values
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1352
1415
|
if (!values.length) {
|
|
1353
1416
|
return this._pushWhere({ type: 'raw', raw: '0 = 1', bindings: [] });
|
|
1354
1417
|
}
|
|
@@ -1391,7 +1454,13 @@ class QueryBuilder {
|
|
|
1391
1454
|
}
|
|
1392
1455
|
|
|
1393
1456
|
whereNotIn(column, values = []) {
|
|
1394
|
-
if (!Array.isArray(values))
|
|
1457
|
+
if (!Array.isArray(values)) {
|
|
1458
|
+
throw new DBError('whereNotIn expects an array', {
|
|
1459
|
+
method: 'whereNotIn',
|
|
1460
|
+
column,
|
|
1461
|
+
values
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1395
1464
|
if (!values.length) {
|
|
1396
1465
|
return this._pushWhere({ type: 'raw', raw: '1 = 1', bindings: [] });
|
|
1397
1466
|
}
|
|
@@ -1576,7 +1645,14 @@ class QueryBuilder {
|
|
|
1576
1645
|
whereHas(relationName, callback, boolean = 'AND') {
|
|
1577
1646
|
const relation = this.modelClass.relations()?.[relationName];
|
|
1578
1647
|
if (!relation) {
|
|
1579
|
-
throw new
|
|
1648
|
+
throw new DBError(
|
|
1649
|
+
`Relation '${relationName}' is not defined`,
|
|
1650
|
+
{
|
|
1651
|
+
model: this.modelClass?.name,
|
|
1652
|
+
relation: relationName,
|
|
1653
|
+
method: 'whereHas'
|
|
1654
|
+
}
|
|
1655
|
+
);
|
|
1580
1656
|
}
|
|
1581
1657
|
|
|
1582
1658
|
const RelatedModel = relation.model();
|
|
@@ -1641,6 +1717,16 @@ class QueryBuilder {
|
|
|
1641
1717
|
parts.push('SELECT');
|
|
1642
1718
|
if (this._distinct) parts.push('DISTINCT');
|
|
1643
1719
|
|
|
1720
|
+
// Normalize SELECT * to table.* when a model is attached
|
|
1721
|
+
if (
|
|
1722
|
+
this.modelClass &&
|
|
1723
|
+
this._select.length === 1 &&
|
|
1724
|
+
this._select[0] === '*'
|
|
1725
|
+
) {
|
|
1726
|
+
const table = this.tableAlias || this.table;
|
|
1727
|
+
this._select = [`${escapeId(table)}.*`];
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1644
1730
|
parts.push(this._select.length ? this._select.join(', ') : '*');
|
|
1645
1731
|
|
|
1646
1732
|
if (this._fromRaw) {
|
|
@@ -1785,7 +1871,10 @@ class QueryBuilder {
|
|
|
1785
1871
|
break;
|
|
1786
1872
|
|
|
1787
1873
|
default:
|
|
1788
|
-
throw new
|
|
1874
|
+
throw new DBError('Unknown where clause type', {
|
|
1875
|
+
type: w.type,
|
|
1876
|
+
where: w
|
|
1877
|
+
});
|
|
1789
1878
|
}
|
|
1790
1879
|
});
|
|
1791
1880
|
|
|
@@ -1823,20 +1912,29 @@ class QueryBuilder {
|
|
|
1823
1912
|
const sql = this._compileSelect();
|
|
1824
1913
|
const binds = this._gatherBindings();
|
|
1825
1914
|
|
|
1826
|
-
|
|
1915
|
+
try {
|
|
1916
|
+
const rows = await DB.raw(sql, binds);
|
|
1917
|
+
|
|
1918
|
+
if (this.modelClass) {
|
|
1919
|
+
const models = rows.map(r => new this.modelClass(r, true));
|
|
1827
1920
|
|
|
1828
|
-
|
|
1829
|
-
|
|
1921
|
+
if (this._with.length) {
|
|
1922
|
+
const loaded = await this._eagerLoad(models);
|
|
1923
|
+
return new Collection(loaded);
|
|
1924
|
+
}
|
|
1830
1925
|
|
|
1831
|
-
|
|
1832
|
-
const loaded = await this._eagerLoad(models);
|
|
1833
|
-
return new Collection(loaded);
|
|
1926
|
+
return new Collection(models);
|
|
1834
1927
|
}
|
|
1835
1928
|
|
|
1836
|
-
return new Collection(
|
|
1929
|
+
return new Collection(rows);
|
|
1930
|
+
} catch (err) {
|
|
1931
|
+
throw new DBError('Select query failed', {
|
|
1932
|
+
table: this.table,
|
|
1933
|
+
sql,
|
|
1934
|
+
bindings: binds,
|
|
1935
|
+
err
|
|
1936
|
+
});
|
|
1837
1937
|
}
|
|
1838
|
-
|
|
1839
|
-
return new Collection(rows);
|
|
1840
1938
|
}
|
|
1841
1939
|
|
|
1842
1940
|
async first() {
|
|
@@ -1849,7 +1947,13 @@ class QueryBuilder {
|
|
|
1849
1947
|
|
|
1850
1948
|
async firstOrFail() {
|
|
1851
1949
|
const r = await this.first();
|
|
1852
|
-
if (!r)
|
|
1950
|
+
if (!r) {
|
|
1951
|
+
throw new DBError('Record not found', {
|
|
1952
|
+
method: 'firstOrFail',
|
|
1953
|
+
table: this.table,
|
|
1954
|
+
model: this.modelClass?.name
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1853
1957
|
return r;
|
|
1854
1958
|
}
|
|
1855
1959
|
|
|
@@ -1860,10 +1964,19 @@ class QueryBuilder {
|
|
|
1860
1964
|
c.limit(1);
|
|
1861
1965
|
|
|
1862
1966
|
const sql = c._compileSelect();
|
|
1863
|
-
const
|
|
1967
|
+
const bindings = c._gatherBindings();
|
|
1864
1968
|
|
|
1865
|
-
|
|
1866
|
-
|
|
1969
|
+
try {
|
|
1970
|
+
const rows = await DB.raw(sql, bindings);
|
|
1971
|
+
return rows.length > 0;
|
|
1972
|
+
} catch (err) {
|
|
1973
|
+
throw new DBError('Exists query failed', {
|
|
1974
|
+
table: this.table,
|
|
1975
|
+
sql,
|
|
1976
|
+
bindings,
|
|
1977
|
+
err
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1867
1980
|
}
|
|
1868
1981
|
|
|
1869
1982
|
async doesntExist() {
|
|
@@ -1878,11 +1991,19 @@ class QueryBuilder {
|
|
|
1878
1991
|
c._offset = null;
|
|
1879
1992
|
|
|
1880
1993
|
const sql = c._compileSelect();
|
|
1881
|
-
const
|
|
1882
|
-
|
|
1883
|
-
const rows = await DB.raw(sql, b);
|
|
1994
|
+
const bindings = c._gatherBindings();
|
|
1884
1995
|
|
|
1885
|
-
|
|
1996
|
+
try {
|
|
1997
|
+
const rows = await DB.raw(sql, bindings);
|
|
1998
|
+
return rows[0] ? Number(rows[0].aggregate) : 0;
|
|
1999
|
+
} catch (err) {
|
|
2000
|
+
throw new DBError('Count query failed', {
|
|
2001
|
+
table: this.table,
|
|
2002
|
+
sql,
|
|
2003
|
+
bindings,
|
|
2004
|
+
err
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
1886
2007
|
}
|
|
1887
2008
|
|
|
1888
2009
|
async _aggregate(expr) {
|
|
@@ -1893,11 +2014,20 @@ class QueryBuilder {
|
|
|
1893
2014
|
c._offset = null;
|
|
1894
2015
|
|
|
1895
2016
|
const sql = c._compileSelect();
|
|
1896
|
-
const
|
|
2017
|
+
const bindings = c._gatherBindings();
|
|
1897
2018
|
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
2019
|
+
try {
|
|
2020
|
+
const rows = await DB.raw(sql, bindings);
|
|
2021
|
+
return rows[0] ? Number(rows[0].aggregate) : 0;
|
|
2022
|
+
} catch (err) {
|
|
2023
|
+
throw new DBError('Aggregate query failed', {
|
|
2024
|
+
table: this.table,
|
|
2025
|
+
expression: expr,
|
|
2026
|
+
sql,
|
|
2027
|
+
bindings,
|
|
2028
|
+
err
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
1901
2031
|
}
|
|
1902
2032
|
|
|
1903
2033
|
sum(c) { return this._aggregate(`SUM(${escapeId(c)})`); }
|
|
@@ -1911,36 +2041,51 @@ class QueryBuilder {
|
|
|
1911
2041
|
c._select = [escapeId(col)];
|
|
1912
2042
|
|
|
1913
2043
|
const sql = c._compileSelect();
|
|
1914
|
-
const
|
|
1915
|
-
|
|
1916
|
-
const rows = await DB.raw(sql, b);
|
|
2044
|
+
const bindings = c._gatherBindings();
|
|
1917
2045
|
|
|
1918
|
-
|
|
2046
|
+
try {
|
|
2047
|
+
const rows = await DB.raw(sql, bindings);
|
|
2048
|
+
return rows.map(r => r[col]);
|
|
2049
|
+
} catch (err) {
|
|
2050
|
+
throw new DBError('Pluck query failed', {
|
|
2051
|
+
table: this.table,
|
|
2052
|
+
column: col,
|
|
2053
|
+
sql,
|
|
2054
|
+
bindings,
|
|
2055
|
+
err
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
1919
2058
|
}
|
|
1920
2059
|
|
|
1921
2060
|
async paginate(page = 1, perPage = 15) {
|
|
1922
2061
|
page = Math.max(1, Number(page));
|
|
1923
2062
|
perPage = Math.max(1, Number(perPage));
|
|
1924
2063
|
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
2064
|
+
try {
|
|
2065
|
+
const countClone = this._clone();
|
|
2066
|
+
countClone._select = [`COUNT(*) AS aggregate`];
|
|
2067
|
+
countClone._orders = [];
|
|
2068
|
+
countClone._limit = null;
|
|
2069
|
+
countClone._offset = null;
|
|
2070
|
+
|
|
2071
|
+
const total = await countClone.count('*');
|
|
2072
|
+
|
|
2073
|
+
const offset = (page - 1) * perPage;
|
|
2074
|
+
|
|
2075
|
+
const rows = await this._clone()
|
|
2076
|
+
.limit(perPage)
|
|
2077
|
+
.offset(offset)
|
|
2078
|
+
.get();
|
|
2079
|
+
|
|
2080
|
+
return new Paginator(rows, total, page, perPage);
|
|
2081
|
+
} catch (err) {
|
|
2082
|
+
throw new DBError('Pagination failed', {
|
|
2083
|
+
table: this.table,
|
|
2084
|
+
page,
|
|
2085
|
+
perPage,
|
|
2086
|
+
err
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
1944
2089
|
}
|
|
1945
2090
|
|
|
1946
2091
|
/**************************************************************************
|
|
@@ -1955,10 +2100,18 @@ class QueryBuilder {
|
|
|
1955
2100
|
`) VALUES (${placeholders})`;
|
|
1956
2101
|
|
|
1957
2102
|
const bindings = Object.values(values);
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
2103
|
+
try {
|
|
2104
|
+
const result = await DB.raw(sql, bindings);
|
|
2105
|
+
|
|
2106
|
+
return result.affectedRows || 0;
|
|
2107
|
+
} catch (err) {
|
|
2108
|
+
throw new DBError('Insert failed', {
|
|
2109
|
+
table: this.table,
|
|
2110
|
+
sql,
|
|
2111
|
+
bindings,
|
|
2112
|
+
err
|
|
2113
|
+
});
|
|
2114
|
+
}
|
|
1962
2115
|
}
|
|
1963
2116
|
|
|
1964
2117
|
async insertGetId(values) {
|
|
@@ -1970,15 +2123,22 @@ class QueryBuilder {
|
|
|
1970
2123
|
`) VALUES (${placeholders})`;
|
|
1971
2124
|
|
|
1972
2125
|
const bindings = Object.values(values);
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
2126
|
+
try {
|
|
2127
|
+
const result = await DB.raw(sql, bindings);
|
|
2128
|
+
|
|
2129
|
+
return result.insertId ?? null;
|
|
2130
|
+
} catch (err) {
|
|
2131
|
+
throw new DBError('Insert (get ID) failed', {
|
|
2132
|
+
table: this.table,
|
|
2133
|
+
sql,
|
|
2134
|
+
bindings,
|
|
2135
|
+
err
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
1977
2138
|
}
|
|
1978
2139
|
|
|
1979
2140
|
async update(values) {
|
|
1980
2141
|
if (!Object.keys(values).length) return 0;
|
|
1981
|
-
|
|
1982
2142
|
const setClause = Object.keys(values)
|
|
1983
2143
|
.map(k => `${escapeId(k)} = ?`)
|
|
1984
2144
|
.join(', ');
|
|
@@ -1988,10 +2148,18 @@ class QueryBuilder {
|
|
|
1988
2148
|
`UPDATE ${escapeId(this.table)} SET ${setClause} ${whereSql}`;
|
|
1989
2149
|
|
|
1990
2150
|
const bindings = [...Object.values(values), ...this._gatherBindings()];
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
2151
|
+
try {
|
|
2152
|
+
const result = await DB.raw(sql, bindings);
|
|
2153
|
+
|
|
2154
|
+
return result.affectedRows || 0;
|
|
2155
|
+
} catch (err) {
|
|
2156
|
+
throw new DBError('Update failed', {
|
|
2157
|
+
table: this.table,
|
|
2158
|
+
sql,
|
|
2159
|
+
bindings,
|
|
2160
|
+
err
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
1995
2163
|
}
|
|
1996
2164
|
|
|
1997
2165
|
async increment(col, by = 1) {
|
|
@@ -2000,11 +2168,20 @@ class QueryBuilder {
|
|
|
2000
2168
|
`SET ${escapeId(col)} = ${escapeId(col)} + ? ` +
|
|
2001
2169
|
this._compileWhereOnly();
|
|
2002
2170
|
|
|
2003
|
-
const
|
|
2004
|
-
|
|
2005
|
-
const res = await DB.raw(sql, b);
|
|
2171
|
+
const bindings = [by, ...this._gatherBindings()];
|
|
2006
2172
|
|
|
2007
|
-
|
|
2173
|
+
try {
|
|
2174
|
+
const res = await DB.raw(sql, bindings);
|
|
2175
|
+
return res.affectedRows || 0;
|
|
2176
|
+
} catch (err) {
|
|
2177
|
+
throw new DBError('Increment failed', {
|
|
2178
|
+
table: this.table,
|
|
2179
|
+
column: col,
|
|
2180
|
+
sql,
|
|
2181
|
+
bindings,
|
|
2182
|
+
err
|
|
2183
|
+
});
|
|
2184
|
+
}
|
|
2008
2185
|
}
|
|
2009
2186
|
|
|
2010
2187
|
async decrement(col, by = 1) {
|
|
@@ -2013,9 +2190,20 @@ class QueryBuilder {
|
|
|
2013
2190
|
`SET ${escapeId(col)} = ${escapeId(col)} - ? ` +
|
|
2014
2191
|
this._compileWhereOnly();
|
|
2015
2192
|
|
|
2016
|
-
const
|
|
2017
|
-
|
|
2018
|
-
|
|
2193
|
+
const bindings = [by, ...this._gatherBindings()];
|
|
2194
|
+
|
|
2195
|
+
try {
|
|
2196
|
+
const res = await DB.raw(sql, bindings);
|
|
2197
|
+
return res.affectedRows || 0;
|
|
2198
|
+
} catch (err) {
|
|
2199
|
+
throw new DBError('Decrement failed', {
|
|
2200
|
+
table: this.table,
|
|
2201
|
+
column: col,
|
|
2202
|
+
sql,
|
|
2203
|
+
bindings,
|
|
2204
|
+
err
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2019
2207
|
}
|
|
2020
2208
|
|
|
2021
2209
|
async delete() {
|
|
@@ -2023,16 +2211,34 @@ class QueryBuilder {
|
|
|
2023
2211
|
`DELETE FROM ${escapeId(this.table)} ` +
|
|
2024
2212
|
this._compileWhereOnly();
|
|
2025
2213
|
|
|
2026
|
-
const
|
|
2214
|
+
const bindings = this._gatherBindings();
|
|
2027
2215
|
|
|
2028
|
-
|
|
2029
|
-
|
|
2216
|
+
try {
|
|
2217
|
+
const res = await DB.raw(sql, bindings);
|
|
2218
|
+
return res.affectedRows || 0;
|
|
2219
|
+
} catch (err) {
|
|
2220
|
+
throw new DBError('Delete failed', {
|
|
2221
|
+
table: this.table,
|
|
2222
|
+
sql,
|
|
2223
|
+
bindings,
|
|
2224
|
+
err
|
|
2225
|
+
});
|
|
2226
|
+
}
|
|
2030
2227
|
}
|
|
2031
2228
|
|
|
2032
2229
|
async truncate() {
|
|
2033
2230
|
const sql = `TRUNCATE TABLE ${escapeId(this.table)}`;
|
|
2034
|
-
|
|
2035
|
-
|
|
2231
|
+
|
|
2232
|
+
try {
|
|
2233
|
+
await DB.raw(sql);
|
|
2234
|
+
return true;
|
|
2235
|
+
} catch (err) {
|
|
2236
|
+
throw new DBError('Truncate failed', {
|
|
2237
|
+
table: this.table,
|
|
2238
|
+
sql,
|
|
2239
|
+
err
|
|
2240
|
+
});
|
|
2241
|
+
}
|
|
2036
2242
|
}
|
|
2037
2243
|
|
|
2038
2244
|
_compileWhereOnly() {
|
|
@@ -2044,19 +2250,23 @@ class QueryBuilder {
|
|
|
2044
2250
|
* EAGER LOAD (unchanged except robust checks)
|
|
2045
2251
|
**************************************************************************/
|
|
2046
2252
|
async _eagerLoad(models) {
|
|
2047
|
-
|
|
2048
|
-
const sample = models[0];
|
|
2049
|
-
if (!sample) return models;
|
|
2253
|
+
if (!models.length) return models;
|
|
2050
2254
|
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
}
|
|
2255
|
+
for (const relName of this._with) {
|
|
2256
|
+
const sample = models.find(m => typeof m[relName] === 'function');
|
|
2257
|
+
if (!sample) continue;
|
|
2055
2258
|
|
|
2056
|
-
const relation =
|
|
2259
|
+
const relation = sample[relName]();
|
|
2057
2260
|
|
|
2058
2261
|
if (!relation || typeof relation.eagerLoad !== 'function') {
|
|
2059
|
-
throw new
|
|
2262
|
+
throw new DBError(
|
|
2263
|
+
`Relation "${relName}" is not eager-loadable`,
|
|
2264
|
+
{
|
|
2265
|
+
model: sample.constructor.name,
|
|
2266
|
+
relation: relName,
|
|
2267
|
+
method: 'eagerLoad'
|
|
2268
|
+
}
|
|
2269
|
+
);
|
|
2060
2270
|
}
|
|
2061
2271
|
|
|
2062
2272
|
await relation.eagerLoad(models, relName);
|
|
@@ -2802,28 +3012,57 @@ class Model {
|
|
|
2802
3012
|
this._relations = {};
|
|
2803
3013
|
this._exists = !!fresh;
|
|
2804
3014
|
|
|
2805
|
-
//
|
|
3015
|
+
// ──────────────────────────────
|
|
3016
|
+
// 1️⃣ hydrate attributes safely
|
|
3017
|
+
// ──────────────────────────────
|
|
2806
3018
|
for (const [k, v] of Object.entries(attributes)) {
|
|
2807
|
-
if (v !== undefined)
|
|
3019
|
+
if (v !== undefined) {
|
|
3020
|
+
this._attributes[k] = v;
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
|
|
3024
|
+
// ──────────────────────────────
|
|
3025
|
+
// 2️⃣ ensure timestamps ALWAYS exist
|
|
3026
|
+
// ──────────────────────────────
|
|
3027
|
+
if (this.constructor.timestamps) {
|
|
3028
|
+
if ('created_at' in attributes)
|
|
3029
|
+
this._attributes.created_at = attributes.created_at;
|
|
3030
|
+
|
|
3031
|
+
if ('updated_at' in attributes)
|
|
3032
|
+
this._attributes.updated_at = attributes.updated_at;
|
|
3033
|
+
}
|
|
3034
|
+
|
|
3035
|
+
// ──────────────────────────────
|
|
3036
|
+
// 3️⃣ always keep primary key
|
|
3037
|
+
// ──────────────────────────────
|
|
3038
|
+
const pk = this.constructor.primaryKey;
|
|
3039
|
+
if (pk && pk in attributes) {
|
|
3040
|
+
this._attributes[pk] = attributes[pk];
|
|
2808
3041
|
}
|
|
2809
3042
|
|
|
2810
3043
|
this._original = { ...this._attributes, ...data };
|
|
2811
3044
|
|
|
2812
|
-
//
|
|
3045
|
+
// ──────────────────────────────
|
|
3046
|
+
// 4️⃣ define getters/setters FOR ALL ATTRIBUTES
|
|
3047
|
+
// ──────────────────────────────
|
|
2813
3048
|
for (const k of Object.keys(this._attributes)) {
|
|
2814
3049
|
if (!(k in this)) {
|
|
2815
3050
|
Object.defineProperty(this, k, {
|
|
2816
|
-
get:
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
3051
|
+
get: () => this._attributes[k],
|
|
3052
|
+
set: v => { this._attributes[k] = v; },
|
|
3053
|
+
enumerable: true,
|
|
3054
|
+
configurable: true
|
|
2820
3055
|
});
|
|
2821
3056
|
}
|
|
2822
3057
|
}
|
|
2823
3058
|
}
|
|
2824
3059
|
|
|
2825
3060
|
static async validate(data, id, ignoreId = null) {
|
|
2826
|
-
if (!Validator)
|
|
3061
|
+
if (!Validator) {
|
|
3062
|
+
throw new DBError('Validator not found', {
|
|
3063
|
+
model: this.name
|
|
3064
|
+
});
|
|
3065
|
+
}
|
|
2827
3066
|
|
|
2828
3067
|
const rules = this.rules || {};
|
|
2829
3068
|
|
|
@@ -2897,20 +3136,6 @@ class Model {
|
|
|
2897
3136
|
}
|
|
2898
3137
|
}
|
|
2899
3138
|
|
|
2900
|
-
// ──────────────────────────────
|
|
2901
|
-
// Query builder accessors
|
|
2902
|
-
// ──────────────────────────────
|
|
2903
|
-
// static query({ withTrashed = false } = {}) {
|
|
2904
|
-
// // use tableName getter (throws if missing)
|
|
2905
|
-
// const qb = new QueryBuilder(this.tableName, this);
|
|
2906
|
-
// if (this.softDeletes && !withTrashed) {
|
|
2907
|
-
// // avoid mutating shared _wheres reference
|
|
2908
|
-
// qb._wheres = Array.isArray(qb._wheres) ? qb._wheres.slice() : [];
|
|
2909
|
-
// qb._wheres.push({ raw: `${DB.escapeId(this.deletedAt)} IS NULL` });
|
|
2910
|
-
// }
|
|
2911
|
-
// return qb;
|
|
2912
|
-
// }
|
|
2913
|
-
|
|
2914
3139
|
static query({ withTrashed = false } = {}) {
|
|
2915
3140
|
const qb = new QueryBuilder(this.tableName, this);
|
|
2916
3141
|
|
|
@@ -3000,14 +3225,26 @@ class Model {
|
|
|
3000
3225
|
}
|
|
3001
3226
|
|
|
3002
3227
|
static async findManyBy(col, values = []) {
|
|
3003
|
-
if (!Array.isArray(values))
|
|
3228
|
+
if (!Array.isArray(values)) {
|
|
3229
|
+
throw new DBError('findManyBy expects an array of values', {
|
|
3230
|
+
method: 'findManyBy',
|
|
3231
|
+
column: col
|
|
3232
|
+
});
|
|
3233
|
+
}
|
|
3004
3234
|
if (!values.length) return [];
|
|
3005
3235
|
return await this.query().whereIn(col, values).get();
|
|
3006
3236
|
}
|
|
3007
3237
|
|
|
3008
3238
|
// additional common accessors
|
|
3009
3239
|
static async findMany(ids = []) {
|
|
3010
|
-
if (!Array.isArray(ids)) ids = [ids];
|
|
3240
|
+
// if (!Array.isArray(ids)) ids = [ids];
|
|
3241
|
+
if (!Array.isArray(ids)) {
|
|
3242
|
+
throw new DBError('findMany expects an array of IDs', {
|
|
3243
|
+
method: 'findMany',
|
|
3244
|
+
model: this.name,
|
|
3245
|
+
ids
|
|
3246
|
+
});
|
|
3247
|
+
}
|
|
3011
3248
|
if (!ids.length) return [];
|
|
3012
3249
|
return await this.query().whereIn(this.primaryKey, ids).get();
|
|
3013
3250
|
}
|
|
@@ -3077,7 +3314,10 @@ class Model {
|
|
|
3077
3314
|
|
|
3078
3315
|
// 3. Block empty payload
|
|
3079
3316
|
if (!Object.keys(payload).length) {
|
|
3080
|
-
throw new DBError('Attempted to create with empty payload'
|
|
3317
|
+
throw new DBError('Attempted to create with empty payload', {
|
|
3318
|
+
model: this.name,
|
|
3319
|
+
attrs
|
|
3320
|
+
});
|
|
3081
3321
|
}
|
|
3082
3322
|
|
|
3083
3323
|
// 4. Create + save
|
|
@@ -3164,9 +3404,9 @@ class Model {
|
|
|
3164
3404
|
// 🛡 SANITIZATION UTIL
|
|
3165
3405
|
// ──────────────────────────────
|
|
3166
3406
|
static isBadValue(value) {
|
|
3167
|
-
if (value === null || value === undefined
|
|
3168
|
-
if (typeof value === 'string' && !value.trim()) return true;
|
|
3169
|
-
return false;
|
|
3407
|
+
if (value === null || value === undefined) return true;
|
|
3408
|
+
if (typeof value === 'string' && !value.trim()) return true; // empty string
|
|
3409
|
+
return false; // allow 0, false, etc.
|
|
3170
3410
|
}
|
|
3171
3411
|
|
|
3172
3412
|
sanitize(attrs = {}) {
|
|
@@ -3177,7 +3417,7 @@ class Model {
|
|
|
3177
3417
|
const val = attrs[key];
|
|
3178
3418
|
|
|
3179
3419
|
if (!this.constructor.isBadValue(val)) {
|
|
3180
|
-
clean[key] = val;
|
|
3420
|
+
clean[key] = val; // keep 0, false, etc.
|
|
3181
3421
|
} else if (keepCols.includes(key)) {
|
|
3182
3422
|
clean[key] = null;
|
|
3183
3423
|
}
|
|
@@ -3191,13 +3431,24 @@ class Model {
|
|
|
3191
3431
|
// ──────────────────────────────
|
|
3192
3432
|
async fill(attrs = {}) {
|
|
3193
3433
|
const allowed = this.constructor.fillable || Object.keys(attrs);
|
|
3434
|
+
let filled = false;
|
|
3194
3435
|
|
|
3195
3436
|
for (const key of Object.keys(attrs)) {
|
|
3196
3437
|
const val = attrs[key];
|
|
3197
|
-
if (allowed.includes(key) &&
|
|
3198
|
-
this._attributes[key] = val;
|
|
3438
|
+
if (allowed.includes(key) && val !== undefined) {
|
|
3439
|
+
this._attributes[key] = val; // 0, false, null preserved
|
|
3440
|
+
filled = true;
|
|
3199
3441
|
}
|
|
3200
3442
|
}
|
|
3443
|
+
|
|
3444
|
+
if (!filled && Object.keys(attrs).length > 0) {
|
|
3445
|
+
throw new DBError('No fillable attributes provided', {
|
|
3446
|
+
model: this.constructor.name,
|
|
3447
|
+
attrs,
|
|
3448
|
+
fillable: allowed
|
|
3449
|
+
});
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3201
3452
|
return this;
|
|
3202
3453
|
}
|
|
3203
3454
|
|
|
@@ -3209,17 +3460,23 @@ class Model {
|
|
|
3209
3460
|
// INSERT – validation first
|
|
3210
3461
|
// ──────────────────────────────
|
|
3211
3462
|
async saveNew(attrs) {
|
|
3463
|
+
if (!this._exists && !Object.keys(this._attributes).length) {
|
|
3464
|
+
throw new DBError('Cannot save empty model', {
|
|
3465
|
+
model: this.constructor.name
|
|
3466
|
+
});
|
|
3467
|
+
}
|
|
3468
|
+
|
|
3212
3469
|
const payload = this.sanitize(attrs || this._attributes);
|
|
3213
3470
|
|
|
3214
|
-
//
|
|
3471
|
+
// validate BEFORE hooks/db
|
|
3215
3472
|
await this.constructor.validate(payload);
|
|
3216
3473
|
await this.trigger('creating');
|
|
3217
3474
|
|
|
3218
3475
|
// timestamps
|
|
3219
3476
|
if (this.constructor.timestamps) {
|
|
3220
3477
|
const now = new Date();
|
|
3221
|
-
payload.created_at
|
|
3222
|
-
payload.updated_at
|
|
3478
|
+
if (payload.created_at === undefined) payload.created_at = now;
|
|
3479
|
+
if (payload.updated_at === undefined) payload.updated_at = now;
|
|
3223
3480
|
}
|
|
3224
3481
|
|
|
3225
3482
|
// soft deletes
|
|
@@ -3231,13 +3488,11 @@ class Model {
|
|
|
3231
3488
|
}
|
|
3232
3489
|
|
|
3233
3490
|
const qb = this.constructor.query();
|
|
3234
|
-
|
|
3235
3491
|
const result = await qb.insert(payload);
|
|
3236
3492
|
|
|
3237
3493
|
// handle pk
|
|
3238
3494
|
const pk = this.constructor.primaryKey;
|
|
3239
3495
|
const insertId = Array.isArray(result) ? result[0] : result;
|
|
3240
|
-
|
|
3241
3496
|
if (!(pk in payload) && insertId !== undefined) {
|
|
3242
3497
|
payload[pk] = insertId;
|
|
3243
3498
|
}
|
|
@@ -3248,12 +3503,16 @@ class Model {
|
|
|
3248
3503
|
|
|
3249
3504
|
return this;
|
|
3250
3505
|
}
|
|
3251
|
-
|
|
3252
3506
|
// ──────────────────────────────
|
|
3253
3507
|
// UPDATE – only dirty fields
|
|
3254
3508
|
// ──────────────────────────────
|
|
3255
3509
|
async save() {
|
|
3256
|
-
if (!this._exists) return this.saveNew(this._attributes);
|
|
3510
|
+
// if (!this._exists) return this.saveNew(this._attributes);
|
|
3511
|
+
if (!this._exists && !Object.keys(this._attributes).length) {
|
|
3512
|
+
throw new DBError('Cannot save empty model', {
|
|
3513
|
+
model: this.constructor.name
|
|
3514
|
+
});
|
|
3515
|
+
}
|
|
3257
3516
|
|
|
3258
3517
|
await this.trigger('updating');
|
|
3259
3518
|
|
|
@@ -3264,11 +3523,8 @@ class Model {
|
|
|
3264
3523
|
for (const key of Object.keys(attrs)) {
|
|
3265
3524
|
const val = attrs[key];
|
|
3266
3525
|
|
|
3267
|
-
if (val !== orig[key] &&
|
|
3268
|
-
if (this.constructor.softDeletes &&
|
|
3269
|
-
key === this.constructor.deletedAt) {
|
|
3270
|
-
continue;
|
|
3271
|
-
}
|
|
3526
|
+
if (val !== orig[key] && val !== undefined) { // only treat undefined as "no change"
|
|
3527
|
+
if (this.constructor.softDeletes && key === this.constructor.deletedAt) continue;
|
|
3272
3528
|
dirty[key] = val;
|
|
3273
3529
|
}
|
|
3274
3530
|
}
|
|
@@ -3285,14 +3541,10 @@ class Model {
|
|
|
3285
3541
|
|
|
3286
3542
|
// validate BEFORE db write
|
|
3287
3543
|
const pk = this.constructor.primaryKey;
|
|
3288
|
-
await this.constructor.validate(
|
|
3289
|
-
{ ...this._original, ...payload },
|
|
3290
|
-
this._attributes[pk]
|
|
3291
|
-
);
|
|
3544
|
+
await this.constructor.validate({ ...this._original, ...payload }, this._attributes[pk]);
|
|
3292
3545
|
|
|
3293
3546
|
const id = this._attributes[pk];
|
|
3294
3547
|
const qb = this.constructor.query();
|
|
3295
|
-
|
|
3296
3548
|
await qb.where(pk, id).update(payload);
|
|
3297
3549
|
|
|
3298
3550
|
this._original = { ...this.sanitize(this._attributes) };
|
|
@@ -3326,8 +3578,13 @@ class Model {
|
|
|
3326
3578
|
let rel;
|
|
3327
3579
|
try {
|
|
3328
3580
|
rel = fn.call(dummy);
|
|
3329
|
-
} catch {
|
|
3330
|
-
continue;
|
|
3581
|
+
} catch (err) {
|
|
3582
|
+
// continue;
|
|
3583
|
+
throw new DBError('Failed to resolve relation', {
|
|
3584
|
+
model: this.name,
|
|
3585
|
+
relation: name,
|
|
3586
|
+
err
|
|
3587
|
+
});
|
|
3331
3588
|
}
|
|
3332
3589
|
|
|
3333
3590
|
if (!rel) continue;
|
|
@@ -3395,8 +3652,13 @@ class Model {
|
|
|
3395
3652
|
// RESTRICT — block delete
|
|
3396
3653
|
// ──────────────────────────────────
|
|
3397
3654
|
case 'restrict':
|
|
3398
|
-
throw new
|
|
3399
|
-
`Cannot delete ${this.constructor.name}: related ${relName} exists
|
|
3655
|
+
throw new DBError(
|
|
3656
|
+
`Cannot delete ${this.constructor.name}: related ${relName} exists`,
|
|
3657
|
+
{
|
|
3658
|
+
model: this.constructor.name,
|
|
3659
|
+
relation: relName,
|
|
3660
|
+
behavior: 'restrict'
|
|
3661
|
+
}
|
|
3400
3662
|
);
|
|
3401
3663
|
|
|
3402
3664
|
// ──────────────────────────────────
|
|
@@ -3458,7 +3720,14 @@ class Model {
|
|
|
3458
3720
|
|
|
3459
3721
|
|
|
3460
3722
|
static async destroy(ids) {
|
|
3461
|
-
if (!Array.isArray(ids))
|
|
3723
|
+
if (!Array.isArray(ids)) {
|
|
3724
|
+
throw new DBError('destroy expects an array of IDs', {
|
|
3725
|
+
model: this.name,
|
|
3726
|
+
ids
|
|
3727
|
+
});
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3730
|
+
// if (!Array.isArray(ids)) ids = [ids];
|
|
3462
3731
|
const pk = this.primaryKey;
|
|
3463
3732
|
|
|
3464
3733
|
// --- Load models so cascade works ---
|
|
@@ -3618,7 +3887,12 @@ class Model {
|
|
|
3618
3887
|
|
|
3619
3888
|
async refresh() {
|
|
3620
3889
|
const pk = this.constructor.primaryKey;
|
|
3621
|
-
if (!this._attributes[pk]) return this;
|
|
3890
|
+
// if (!this._attributes[pk]) return this;
|
|
3891
|
+
if (!this._attributes[pk]) {
|
|
3892
|
+
throw new DBError('Cannot refresh model without primary key', {
|
|
3893
|
+
model: this.constructor.name
|
|
3894
|
+
});
|
|
3895
|
+
}
|
|
3622
3896
|
const fresh = await this.constructor.find(this._attributes[pk]);
|
|
3623
3897
|
if (fresh) {
|
|
3624
3898
|
this._attributes = { ...fresh._attributes };
|
|
@@ -3642,6 +3916,158 @@ class Model {
|
|
|
3642
3916
|
}
|
|
3643
3917
|
}
|
|
3644
3918
|
|
|
3919
|
+
class Session extends Model {
|
|
3920
|
+
static table = 'sessions';
|
|
3921
|
+
static primaryKey = 'sid';
|
|
3922
|
+
static slugKey = null;
|
|
3923
|
+
static timestamps = false;
|
|
3924
|
+
|
|
3925
|
+
static fillable = ['sid', 'data', 'expires'];
|
|
3926
|
+
}
|
|
3927
|
+
|
|
3928
|
+
const session = require('express-session');
|
|
3929
|
+
|
|
3930
|
+
class LamixSessionStore extends session.Store {
|
|
3931
|
+
constructor(options = {}) {
|
|
3932
|
+
super();
|
|
3933
|
+
|
|
3934
|
+
this.ttl = options.ttl || 86400; // seconds
|
|
3935
|
+
this.cleanupInterval = options.cleanupInterval || 60000;
|
|
3936
|
+
|
|
3937
|
+
this._startCleanup();
|
|
3938
|
+
}
|
|
3939
|
+
|
|
3940
|
+
/* ---------- Get ---------- */
|
|
3941
|
+
|
|
3942
|
+
async get(sid, cb) {
|
|
3943
|
+
try {
|
|
3944
|
+
const now = Date.now();
|
|
3945
|
+
|
|
3946
|
+
const row = await Session
|
|
3947
|
+
.query()
|
|
3948
|
+
.where('sid', sid)
|
|
3949
|
+
.where('expires', '>', now)
|
|
3950
|
+
.first();
|
|
3951
|
+
|
|
3952
|
+
if (!row) return cb(null, null);
|
|
3953
|
+
|
|
3954
|
+
cb(null, JSON.parse(row.data));
|
|
3955
|
+
} catch (err) {
|
|
3956
|
+
cb(new DBError('Failed to load session', {
|
|
3957
|
+
sid,
|
|
3958
|
+
operation: 'get',
|
|
3959
|
+
err
|
|
3960
|
+
}));
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
|
|
3964
|
+
/* ---------- Set ---------- */
|
|
3965
|
+
|
|
3966
|
+
async set(sid, sessionData, cb) {
|
|
3967
|
+
try {
|
|
3968
|
+
const expires =
|
|
3969
|
+
sessionData.cookie?.expires
|
|
3970
|
+
? new Date(sessionData.cookie.expires).getTime()
|
|
3971
|
+
: Date.now() + this.ttl * 1000;
|
|
3972
|
+
|
|
3973
|
+
const payload = {
|
|
3974
|
+
sid,
|
|
3975
|
+
data: JSON.stringify(sessionData),
|
|
3976
|
+
expires
|
|
3977
|
+
};
|
|
3978
|
+
|
|
3979
|
+
// const existing = await Session.find(sid);
|
|
3980
|
+
const existing = await Session
|
|
3981
|
+
.query()
|
|
3982
|
+
.where('sid', sid)
|
|
3983
|
+
.first();
|
|
3984
|
+
|
|
3985
|
+
if (existing) {
|
|
3986
|
+
await existing.update(payload);
|
|
3987
|
+
} else {
|
|
3988
|
+
const session = new Session(payload, false);
|
|
3989
|
+
await session.saveNew(payload);
|
|
3990
|
+
}
|
|
3991
|
+
|
|
3992
|
+
cb(null);
|
|
3993
|
+
} catch (err) {
|
|
3994
|
+
cb(new DBError('Failed to persist session', {
|
|
3995
|
+
sid,
|
|
3996
|
+
operation: 'set',
|
|
3997
|
+
err
|
|
3998
|
+
}));
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
4001
|
+
|
|
4002
|
+
/* ---------- Destroy ---------- */
|
|
4003
|
+
|
|
4004
|
+
async destroy(sid, cb) {
|
|
4005
|
+
try {
|
|
4006
|
+
await Session
|
|
4007
|
+
.query()
|
|
4008
|
+
.where('sid', sid)
|
|
4009
|
+
.delete();
|
|
4010
|
+
|
|
4011
|
+
cb(null);
|
|
4012
|
+
} catch (err) {
|
|
4013
|
+
cb(new DBError('Failed to destroy session', {
|
|
4014
|
+
sid,
|
|
4015
|
+
operation: 'destroy',
|
|
4016
|
+
err
|
|
4017
|
+
}));
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
4020
|
+
|
|
4021
|
+
/* ---------- Touch ---------- */
|
|
4022
|
+
|
|
4023
|
+
async touch(sid, sessionData, cb) {
|
|
4024
|
+
if (!sessionData) return cb(null);
|
|
4025
|
+
|
|
4026
|
+
try {
|
|
4027
|
+
const expires =
|
|
4028
|
+
sessionData.cookie?.expires
|
|
4029
|
+
? new Date(sessionData.cookie.expires).getTime()
|
|
4030
|
+
: Date.now() + this.ttl * 1000;
|
|
4031
|
+
|
|
4032
|
+
await Session
|
|
4033
|
+
.query()
|
|
4034
|
+
.where('sid', sid)
|
|
4035
|
+
.update({ expires });
|
|
4036
|
+
|
|
4037
|
+
cb();
|
|
4038
|
+
} catch (err) {
|
|
4039
|
+
cb(new DBError('Failed to touch session', {
|
|
4040
|
+
sid,
|
|
4041
|
+
operation: 'touch',
|
|
4042
|
+
err
|
|
4043
|
+
}));
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
|
|
4047
|
+
/* ---------- Cleanup ---------- */
|
|
4048
|
+
|
|
4049
|
+
_startCleanup() {
|
|
4050
|
+
this._cleanupTimer = setInterval(async () => {
|
|
4051
|
+
try {
|
|
4052
|
+
await Session
|
|
4053
|
+
.query()
|
|
4054
|
+
.where('expires', '<', Date.now())
|
|
4055
|
+
.delete();
|
|
4056
|
+
} catch (err) {
|
|
4057
|
+
// cleanup must NEVER fail silently
|
|
4058
|
+
throw new DBError('Session cleanup failed', {
|
|
4059
|
+
operation: 'cleanup',
|
|
4060
|
+
err
|
|
4061
|
+
});
|
|
4062
|
+
}
|
|
4063
|
+
}, this.cleanupInterval);
|
|
4064
|
+
|
|
4065
|
+
this._cleanupTimer.unref();
|
|
4066
|
+
}
|
|
4067
|
+
|
|
4068
|
+
}
|
|
4069
|
+
|
|
4070
|
+
|
|
3645
4071
|
// --- BaseModel with bcrypt hashing ---
|
|
3646
4072
|
const bcrypt = tryRequire('bcrypt');
|
|
3647
4073
|
class BaseModel extends Model {
|
|
@@ -3730,4 +4156,4 @@ class BaseModel extends Model {
|
|
|
3730
4156
|
}
|
|
3731
4157
|
}
|
|
3732
4158
|
|
|
3733
|
-
module.exports = { DB, Model, Validator, ValidationError, Collection, QueryBuilder, HasMany, HasOne, BelongsTo, BelongsToMany, DBError, BaseModel};
|
|
4159
|
+
module.exports = { DB, Model, Validator, ValidationError, Collection, QueryBuilder, HasMany, HasOne, BelongsTo, BelongsToMany, DBError, LamixSessionStore, BaseModel};
|