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/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
- const m = tryRequire('mysql2/promise');
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
- const m = tryRequire('pg');
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
- const m = tryRequire('sqlite3');
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
- this._emit('error', { err, sql, bindings, attempt });
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
- this._als.getStore()?.inTransaction;
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('DB query failed', { sql, bindings, err });
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 Error(`Invalid SQL operator: ${operator}`);
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)) throw new Error('whereIn expects array');
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)) throw new Error('whereNotIn expects array');
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 Error(`Relation '${relationName}' is not defined on ${this.modelClass.name}`);
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 Error('Unknown where type: ' + w.type);
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
- const rows = await DB.raw(sql, binds);
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
- if (this.modelClass) {
1829
- const models = rows.map(r => new this.modelClass(r, true));
1921
+ if (this._with.length) {
1922
+ const loaded = await this._eagerLoad(models);
1923
+ return new Collection(loaded);
1924
+ }
1830
1925
 
1831
- if (this._with.length) {
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(models);
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) throw new Error('Record not found');
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 b = c._gatherBindings();
1967
+ const bindings = c._gatherBindings();
1864
1968
 
1865
- const rows = await DB.raw(sql, b);
1866
- return rows.length > 0;
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 b = c._gatherBindings();
1882
-
1883
- const rows = await DB.raw(sql, b);
1994
+ const bindings = c._gatherBindings();
1884
1995
 
1885
- return rows[0] ? Number(rows[0].aggregate) : 0;
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 b = c._gatherBindings();
2017
+ const bindings = c._gatherBindings();
1897
2018
 
1898
- const rows = await DB.raw(sql, b);
1899
-
1900
- return rows[0] ? Number(rows[0].aggregate) : 0;
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 b = c._gatherBindings();
1915
-
1916
- const rows = await DB.raw(sql, b);
2044
+ const bindings = c._gatherBindings();
1917
2045
 
1918
- return rows.map(r => r[col]);
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
- // Build a clone to compute total (safe — uses your existing _clone and count)
1926
- const countClone = this._clone();
1927
- countClone._select = [`COUNT(*) AS aggregate`];
1928
- countClone._orders = [];
1929
- countClone._limit = null;
1930
- countClone._offset = null;
1931
-
1932
- const total = await countClone.count('*');
1933
-
1934
- const offset = (page - 1) * perPage;
1935
-
1936
- // Use a clone to fetch rows so we don't mutate caller's builder
1937
- const rows = await this._clone()
1938
- .limit(perPage)
1939
- .offset(offset)
1940
- .get();
1941
-
1942
- // Return Paginator instance with .toJSON()
1943
- return new Paginator(rows, total, page, perPage);
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
- const result = await DB.raw(sql, bindings);
1960
-
1961
- return result.affectedRows || 0;
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
- const result = await DB.raw(sql, bindings);
1975
-
1976
- return result.insertId ?? null;
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
- const result = await DB.raw(sql, bindings);
1993
-
1994
- return result.affectedRows || 0;
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 b = [by, ...this._gatherBindings()];
2004
-
2005
- const res = await DB.raw(sql, b);
2171
+ const bindings = [by, ...this._gatherBindings()];
2006
2172
 
2007
- return res.affectedRows || 0;
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 b = [by, ...this._gatherBindings()];
2017
- const res = await DB.raw(sql, b);
2018
- return res.affectedRows || 0;
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 b = this._gatherBindings();
2214
+ const bindings = this._gatherBindings();
2027
2215
 
2028
- const res = await DB.raw(sql, b);
2029
- return res.affectedRows || 0;
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
- await DB.raw(sql);
2035
- return true;
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
- for (const relName of this._with) {
2048
- const sample = models[0];
2049
- if (!sample) return models;
2253
+ if (!models.length) return models;
2050
2254
 
2051
- const relationMethod = sample[relName];
2052
- if (typeof relationMethod !== 'function') {
2053
- throw new Error(`Relation "${relName}" is not a method on ${sample.constructor.name}`);
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 = relationMethod.call(sample);
2259
+ const relation = sample[relName]();
2057
2260
 
2058
2261
  if (!relation || typeof relation.eagerLoad !== 'function') {
2059
- throw new Error(`Relation "${relName}" does not have a valid eagerLoad method`);
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
- // Only store keys with defined values
3015
+ // ──────────────────────────────
3016
+ // 1️⃣ hydrate attributes safely
3017
+ // ──────────────────────────────
2806
3018
  for (const [k, v] of Object.entries(attributes)) {
2807
- if (v !== undefined) this._attributes[k] = v;
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
- // Define getters for attributes
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: function() {
2817
- return this._attributes[k];
2818
- },
2819
- enumerable: true
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) throw new Error('Validator not found.');
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)) throw new Error('findManyBy expects an array of 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 || value === '') return true;
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) && !this.constructor.isBadValue(val)) {
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
- // Validate BEFORE hooks/db
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 = payload.created_at || now;
3222
- payload.updated_at = payload.updated_at || now;
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] && !this.constructor.isBadValue(val)) {
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 Error(
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)) ids = [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};