millas 0.2.21 → 0.2.23

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "millas",
3
- "version": "0.2.21",
3
+ "version": "0.2.23",
4
4
  "description": "A modern batteries-included backend framework for Node.js — built on Express, inspired by Laravel, Django, and FastAPI",
5
5
  "main": "src/index.js",
6
6
  "exports": {
@@ -81,7 +81,18 @@ class EventEmitter {
81
81
  async _invoke(handler, event) {
82
82
  // Listener class (has handle() on prototype)
83
83
  if (typeof handler === 'function' && typeof handler.prototype?.handle === 'function') {
84
- const inst = new handler();
84
+ // Resolve static inject dependencies from the container
85
+ let inst;
86
+ if (handler.inject && Array.isArray(handler.inject) && handler.inject.length) {
87
+ const Facade = require('../facades/Facade');
88
+ const container = Facade._container;
89
+ const deps = handler.inject.map(key => {
90
+ try { return container ? container.make(key) : undefined; } catch { return undefined; }
91
+ });
92
+ inst = new handler(...deps);
93
+ } else {
94
+ inst = new handler();
95
+ }
85
96
  if (handler.queue && this._queue) {
86
97
  const Job = require('../queue/Job');
87
98
  const q = this._queue;
@@ -1,43 +1,64 @@
1
1
  'use strict';
2
2
 
3
+ const Facade = require('./Facade');
4
+
3
5
  /**
4
- * millas/facades/Database
6
+ * Database facade — direct access to the knex connection.
7
+ *
8
+ * Usage:
9
+ * const Database = require('millas/facades/Database');
5
10
  *
6
- * ORM models, field definitions, and query builder.
11
+ * // Raw SQL
12
+ * const result = await Database.raw('SELECT NOW()');
7
13
  *
8
- * const { Model, fields } = require('millas/facades/Database');
14
+ * // Knex query builder
15
+ * const rows = await Database.table('posts').where('published', true).select('*');
9
16
  *
10
- * class Post extends Model {
11
- * static table = 'posts';
12
- * static fields = {
13
- * id: fields.id(),
14
- * title: fields.string({ max: 255 }),
15
- * author: fields.ForeignKey('User', { relatedName: 'posts' }),
16
- * published: fields.boolean({ default: false }),
17
- * created_at: fields.timestamp(),
18
- * updated_at: fields.timestamp(),
19
- * };
20
- * }
17
+ * // Named connection
18
+ * const rows = await Database.connection('replica').raw('SELECT 1');
21
19
  */
20
+ class Database {
21
+ static _resolveInstance() {
22
+ const DatabaseManager = require('../orm/drivers/DatabaseManager');
23
+ return DatabaseManager.connection();
24
+ }
25
+ }
26
+
27
+ // Proxy every static call to the knex connection
28
+ module.exports = new Proxy(Database, {
29
+ get(target, prop) {
30
+ // Let real static members through
31
+ if (prop in target || prop === 'then' || prop === 'catch') {
32
+ return target[prop];
33
+ }
34
+ if (typeof prop === 'symbol') return target[prop];
35
+
36
+ // Special case: raw() — normalize result across dialects
37
+ if (prop === 'raw') {
38
+ return async (sql, bindings) => {
39
+ const db = Database._resolveInstance();
40
+ const result = await db.raw(sql, bindings);
41
+ // Postgres returns { rows: [...], command, rowCount, ... }
42
+ // SQLite/MySQL return [rows, fields] or just rows
43
+ if (result && result.rows) return result.rows;
44
+ if (Array.isArray(result)) return result[0] ?? result;
45
+ return result;
46
+ };
47
+ }
22
48
 
23
- const {
24
- Model,
25
- fields,
26
- QueryBuilder,
27
- DatabaseManager,
28
- SchemaBuilder,
29
- MigrationRunner,
30
- ModelInspector,
31
- DatabaseServiceProvider,
32
- } = require('../core');
49
+ // Special case: connection(name) returns a named knex instance
50
+ if (prop === 'connection') {
51
+ return (name) => {
52
+ const DatabaseManager = require('../orm/drivers/DatabaseManager');
53
+ return DatabaseManager.connection(name || null);
54
+ };
55
+ }
33
56
 
34
- module.exports = {
35
- Model,
36
- fields,
37
- QueryBuilder,
38
- DatabaseManager,
39
- SchemaBuilder,
40
- MigrationRunner,
41
- ModelInspector,
42
- DatabaseServiceProvider,
43
- };
57
+ // Proxy everything else to the default knex connection
58
+ return (...args) => {
59
+ const db = Database._resolveInstance();
60
+ if (typeof db[prop] !== 'function') return db[prop];
61
+ return db[prop](...args);
62
+ };
63
+ },
64
+ });
@@ -101,6 +101,24 @@ const fields = {
101
101
  return new FieldDefinition('json', options);
102
102
  },
103
103
 
104
+ /**
105
+ * Array field — stores an ordered list of values.
106
+ *
107
+ * On PostgreSQL: uses native ARRAY type (text[], integer[], etc.)
108
+ * On SQLite/MySQL: falls back to JSON column (same as fields.json())
109
+ *
110
+ * @param {string} [of='text'] — item type: 'text' | 'integer' | 'float' | 'boolean'
111
+ *
112
+ * @example
113
+ * tags: fields.array() // text[] on PG, json on SQLite
114
+ * scores: fields.array('integer') // integer[] on PG
115
+ * media_ids: fields.array('integer', { nullable: true, default: [] })
116
+ */
117
+ array(of = 'text', options = {}) {
118
+ if (typeof of === 'object') { options = of; of = 'text'; }
119
+ return new FieldDefinition('array', { arrayOf: of, nullable: true, default: [], ...options });
120
+ },
121
+
104
122
  date(options = {}) {
105
123
  return new FieldDefinition('date', options);
106
124
  },
@@ -408,6 +408,12 @@ ${opsCode},
408
408
  case 'id':
409
409
  return 'fields.id()';
410
410
 
411
+ case 'array': {
412
+ const of_ = def.arrayOf || 'text';
413
+ const optsStr = Object.keys(opts).length ? `, ${this._renderOpts(opts)}` : '';
414
+ return `fields.array('${of_}'${optsStr})`;
415
+ }
416
+
411
417
  case 'enum': {
412
418
  const vals = JSON.stringify(def.enumValues || []);
413
419
  const optsStr = Object.keys(opts).length
@@ -523,6 +523,10 @@ ${this._renderColumn(' ', diff.column, diff.previous, '.alter()')}
523
523
  line = `t.json('${name}')`;
524
524
  break;
525
525
 
526
+ case 'array':
527
+ line = `t.specificType('${name}', '${field.arrayOf || 'text'}[]')`;
528
+ break;
529
+
526
530
  case 'date':
527
531
  line = `t.date('${name}')`;
528
532
  break;
@@ -93,6 +93,24 @@ function _buildColumn(t, name, def, opts = {}) {
93
93
  case 'json':
94
94
  return t.json(name);
95
95
 
96
+ case 'array': {
97
+ const client = t.client?.config?.client || '';
98
+ if (client.includes('pg') || client.includes('postgres')) {
99
+ // Native Postgres ARRAY type
100
+ const pgTypeMap = {
101
+ text: 'text', string: 'text',
102
+ integer: 'integer', int: 'integer',
103
+ float: 'float', decimal: 'decimal',
104
+ boolean: 'boolean',
105
+ uuid: 'uuid',
106
+ };
107
+ const pgType = pgTypeMap[def.arrayOf || 'text'] || 'text';
108
+ return t.specificType(name, `${pgType}[]`);
109
+ }
110
+ // SQLite / MySQL — fall back to JSON
111
+ return t.json(name);
112
+ }
113
+
96
114
  case 'date':
97
115
  return t.date(name);
98
116
 
@@ -957,7 +957,8 @@ class Model {
957
957
  case 'bigInteger': return typeof val === 'bigint' ? val : parseInt(val, 10);
958
958
  case 'float':
959
959
  case 'decimal': return typeof val === 'number' ? val : parseFloat(val);
960
- case 'json': return typeof val === 'string' ? JSON.parse(val) : val;
960
+ case 'json':
961
+ case 'array': return typeof val === 'string' ? JSON.parse(val) : (Array.isArray(val) ? val : (val ?? []));
961
962
  case 'date':
962
963
  case 'timestamp': {
963
964
  if (val instanceof Date) return isNaN(val.getTime()) ? null : val;
@@ -981,7 +982,7 @@ class Model {
981
982
 
982
983
  static _serializeValue(val, type) {
983
984
  if (val == null) return val;
984
- if (type === 'json') return typeof val === 'string' ? val : JSON.stringify(val);
985
+ if (type === 'json' || type === 'array') return typeof val === 'string' ? val : JSON.stringify(val);
985
986
  if (type === 'boolean') return val ? 1 : 0;
986
987
  if ((type === 'date' || type === 'timestamp') && val instanceof Date) return val.toISOString();
987
988
  return val;
@@ -253,6 +253,27 @@ class QueryBuilder {
253
253
  return this;
254
254
  }
255
255
 
256
+ join(table, col1, col2, col3) {
257
+ this._query = col3
258
+ ? this._query.join(table, col1, col2, col3)
259
+ : this._query.join(table, col1, col2);
260
+ return this;
261
+ }
262
+
263
+ leftJoin(table, col1, col2, col3) {
264
+ this._query = col3
265
+ ? this._query.leftJoin(table, col1, col2, col3)
266
+ : this._query.leftJoin(table, col1, col2);
267
+ return this;
268
+ }
269
+
270
+ rightJoin(table, col1, col2, col3) {
271
+ this._query = col3
272
+ ? this._query.rightJoin(table, col1, col2, col3)
273
+ : this._query.rightJoin(table, col1, col2);
274
+ return this;
275
+ }
276
+
256
277
  having(column, operatorOrValue, value) {
257
278
  if (value !== undefined) {
258
279
  this._query = this._query.having(column, operatorOrValue, value);
@@ -561,6 +582,38 @@ class QueryBuilder {
561
582
  continue;
562
583
  }
563
584
 
585
+ // -- Nested dot notation: 'documents.document_images'
586
+ // Matches Django's prefetch_related('documents__document_images')
587
+ if (name.includes('.')) {
588
+ const [parentRel, ...rest] = name.split('.');
589
+ const nestedName = rest.join('.');
590
+ const parentRelDef = relations[parentRel];
591
+ if (!parentRelDef) {
592
+ MillasLog.w('ORM', `Relation "${parentRel}" not defined on ${this._model.name} -- skipping nested eager load`);
593
+ continue;
594
+ }
595
+ const alreadyLoaded = instances[0]?.[parentRel] !== undefined &&
596
+ typeof instances[0]?.[parentRel] !== 'function';
597
+ if (!alreadyLoaded) {
598
+ await parentRelDef.eagerLoad(instances, parentRel, null);
599
+ }
600
+ const parentInstances = instances.flatMap(i => {
601
+ const val = i[parentRel];
602
+ if (!val) return [];
603
+ return Array.isArray(val) ? val : [val];
604
+ });
605
+ if (parentInstances.length) {
606
+ const RelatedModel = parentRelDef._related;
607
+ if (RelatedModel) {
608
+ const QB = require('./QueryBuilder');
609
+ const subQb = new QB(RelatedModel._db(), RelatedModel);
610
+ subQb._withs = [{ name: nestedName, constraint: null }];
611
+ await subQb._eagerLoad(parentInstances);
612
+ }
613
+ }
614
+ continue;
615
+ }
616
+
564
617
  // ── Normal eager load ─────────────────────────────────────────────────
565
618
  const rel = relations[name];
566
619
  if (!rel) {