millas 0.2.20 → 0.2.21

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.20",
3
+ "version": "0.2.21",
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": {
@@ -145,7 +145,7 @@ class QueryEngine {
145
145
  // ── Execute data + count in parallel ──────────────────────────────────
146
146
  const [rows, countResult] = await Promise.all([
147
147
  q.clone().limit(limit).offset(offset),
148
- q.clone().count('* as count').first(),
148
+ q.clone().clearOrder().clearSelect().count('* as count').first(),
149
149
  ]);
150
150
 
151
151
  const total = Number(countResult?.count ?? 0);
@@ -231,12 +231,12 @@ class QueryEngine {
231
231
  case 'in': return q.whereIn(col, Array.isArray(value) ? value : [value]);
232
232
  case 'notin': return q.whereNotIn(col, Array.isArray(value) ? value : [value]);
233
233
  case 'between': return q.whereBetween(col, Array.isArray(value) ? value : [value, value]);
234
- case 'contains':
235
- case 'icontains': return q.where(col, 'like', `%${value}%`);
236
- case 'startswith':
237
- case 'istartswith': return q.where(col, 'like', `${value}%`);
238
- case 'endswith':
239
- case 'iendswith': return q.where(col, 'like', `%${value}`);
234
+ case 'contains': return q.where(col, 'like', `%${value}%`);
235
+ case 'icontains': return q.where(q.client?.config?.client?.includes('pg') ? col : col, q.client?.config?.client?.includes('pg') ? 'ilike' : 'like', `%${value}%`);
236
+ case 'startswith': return q.where(col, 'like', `${value}%`);
237
+ case 'istartswith': return q.where(col, q.client?.config?.client?.includes('pg') ? 'ilike' : 'like', `${value}%`);
238
+ case 'endswith': return q.where(col, 'like', `%${value}`);
239
+ case 'iendswith': return q.where(col, q.client?.config?.client?.includes('pg') ? 'ilike' : 'like', `%${value}`);
240
240
  default: return q.where(key, value);
241
241
  }
242
242
  }
@@ -246,15 +246,19 @@ class QueryEngine {
246
246
  * Uses strftime for SQLite/MySQL; falls back gracefully on PostgreSQL.
247
247
  */
248
248
  _applyDateHierarchy(q, col, year, month) {
249
+ const client = q.client?.config?.client || 'sqlite3';
250
+ const isPg = client.includes('pg') || client.includes('postgres');
251
+ const isMy = client.includes('mysql') || client.includes('maria');
252
+
249
253
  if (year) {
250
- try {
251
- q = q.whereRaw(`strftime('%Y', \`${col}\`) = ?`, [String(year)]);
252
- } catch { /* PG fallback — skip */ }
254
+ if (isPg) q = q.whereRaw(`EXTRACT(YEAR FROM "${col}") = ?`, [Number(year)]);
255
+ else if (isMy) q = q.whereRaw(`YEAR(\`${col}\`) = ?`, [Number(year)]);
256
+ else q = q.whereRaw(`strftime('%Y', \`${col}\`) = ?`, [String(year)]);
253
257
  }
254
258
  if (month) {
255
- try {
256
- q = q.whereRaw(`strftime('%m', \`${col}\`) = ?`, [String(month).padStart(2, '0')]);
257
- } catch { /* PG fallback — skip */ }
259
+ if (isPg) q = q.whereRaw(`EXTRACT(MONTH FROM "${col}") = ?`, [Number(month)]);
260
+ else if (isMy) q = q.whereRaw(`MONTH(\`${col}\`) = ?`, [Number(month)]);
261
+ else q = q.whereRaw(`strftime('%m', \`${col}\`) = ?`, [String(month).padStart(2, '0')]);
258
262
  }
259
263
  return q;
260
264
  }
package/src/cli.js CHANGED
@@ -1,5 +1,8 @@
1
1
  'use strict';
2
2
 
3
+ // Load .env before anything else so all commands have access to env vars
4
+ require('dotenv').config();
5
+
3
6
  const { Command } = require('commander');
4
7
  const chalk = require('chalk');
5
8
  const program = new Command();
package/src/core/db.js CHANGED
@@ -1,9 +1,10 @@
1
- const {
2
- Model,
3
- fields
4
- } = require("../orm");
5
- const { migrations } = require("../orm/migration/operations");
1
+ const { Model, fields } = require('../orm');
2
+ const { migrations } = require('../orm/migration/operations');
3
+ const F = require('../orm/query/F');
4
+ const Q = require('../orm/query/Q');
5
+ const HasMany = require('../orm/relations/HasMany');
6
+ const BelongsTo = require('../orm/relations/BelongsTo');
7
+ const HasOne = require('../orm/relations/HasOne');
8
+ const BelongsToMany = require('../orm/relations/BelongsToMany');
6
9
 
7
- module.exports = {
8
- Model, fields,migrations
9
- }
10
+ module.exports = { Model, fields, migrations, F, Q, HasMany, BelongsTo, HasOne, BelongsToMany };
@@ -99,6 +99,12 @@ class DatabaseManager {
99
99
  });
100
100
 
101
101
  case 'mysql':
102
+ try { require('mysql2'); } catch {
103
+ throw new Error(
104
+ 'MySQL driver not installed.\n' +
105
+ 'Run: npm install mysql2'
106
+ );
107
+ }
102
108
  return knex({
103
109
  client: 'mysql2',
104
110
  connection: {
@@ -112,6 +118,12 @@ class DatabaseManager {
112
118
  });
113
119
 
114
120
  case 'postgres':
121
+ try { require('pg'); } catch {
122
+ throw new Error(
123
+ 'PostgreSQL driver not installed.\n' +
124
+ 'Run: npm install pg'
125
+ );
126
+ }
115
127
  return knex({
116
128
  client: 'pg',
117
129
  connection: {
@@ -1,81 +1,49 @@
1
1
  'use strict';
2
2
 
3
- /**
4
- * column.js
5
- *
6
- * Knex column builder helpers shared across all field-level operations.
7
- *
8
- * Having these in one place means:
9
- * - The type → knex method mapping is never duplicated
10
- * - AlterField reuses the same logic as AddField, with `.alter()` appended
11
- * - FK constraint attachment is explicit and separated from column creation
12
- *
13
- * Exports:
14
- * applyColumn(t, name, def) — add a new column to a table builder
15
- * alterColumn(t, name, def) — modify an existing column (.alter())
16
- * attachFKConstraints(db, table, fields) — attach FK constraints via ALTER TABLE
17
- * after all tables in a migration exist
18
- */
19
-
20
3
  // ─── Core column builder ──────────────────────────────────────────────────────
21
4
 
22
- /**
23
- * Add a single column to a knex table builder.
24
- *
25
- * Handles all supported field types, nullability, uniqueness, defaults,
26
- * and inline FK constraints (references).
27
- *
28
- * Pass `{ ...def, references: null }` to suppress FK constraint creation
29
- * when deferring constraints to a later ALTER TABLE pass.
30
- *
31
- * @param {object} t — knex table builder (from createTable / table callback)
32
- * @param {string} name — column name
33
- * @param {object} def — normalised field definition from ProjectState.normaliseField()
34
- */
35
- function applyColumn(t, name, def) {
5
+ function applyColumn(t, name, def, tableName) {
36
6
  const col = _buildColumn(t, name, def);
37
- if (!col) return; // 'id' handled internally by _buildColumn
7
+ if (!col) return;
38
8
 
39
9
  _applyModifiers(col, def);
10
+
11
+ // Postgres enum: stored as text + separate CHECK constraint
12
+ if (def.type === 'enum' && def.enumValues?.length) {
13
+ const client = t.client?.config?.client || '';
14
+ if (client.includes('pg') || client.includes('postgres')) {
15
+ const values = def.enumValues.map(v => `'${v}'`).join(', ');
16
+ const constraintName = `${tableName || 'tbl'}_${name}_check`;
17
+ t.check(`"${name}" in (${values})`, [], constraintName);
18
+ }
19
+ }
40
20
  }
41
21
 
42
- /**
43
- * Modify an existing column in a knex alterTable builder.
44
- * Identical to applyColumn but appends `.alter()` — required by knex to
45
- * signal that this is a column modification, not a new column addition.
46
- *
47
- * Note: FK constraints are NOT altered here use attachFKConstraints()
48
- * to manage them separately. Most DBs require DROP CONSTRAINT + re-add
49
- * for FK changes, which is safer to do explicitly.
50
- *
51
- * @param {object} t knex table builder (from alterTable callback)
52
- * @param {string} name — column name
53
- * @param {object} def normalised field definition
54
- */
55
- function alterColumn(t, name, def) {
22
+ function alterColumn(t, name, def, tableName) {
23
+ const client = t.client?.config?.client || '';
24
+ const isPg = client.includes('pg') || client.includes('postgres');
25
+
26
+ if (isPg && def.type === 'enum' && def.enumValues?.length) {
27
+ // Postgres: ALTER COLUMN TYPE with inline CHECK is invalid.
28
+ // Drop old CHECK constraint, add new one.
29
+ const constraintName = `${tableName || 'tbl'}_${name}_check`;
30
+ const values = def.enumValues.map(v => `'${v}'`).join(', ');
31
+ try { t.dropChecks(constraintName); } catch {}
32
+ t.check(`"${name}" in (${values})`, [], constraintName);
33
+ if (def.nullable) t.setNullable(name);
34
+ else t.dropNullable(name);
35
+ return;
36
+ }
37
+
56
38
  const col = _buildColumn(t, name, def, { forAlter: true });
57
39
  if (!col) return;
58
40
 
59
- _applyModifiers(col, def, { skipFK: true }); // FKs not altered inline
41
+ _applyModifiers(col, def, { skipFK: true });
60
42
  col.alter();
61
43
  }
62
44
 
63
- /**
64
- * Attach FK constraints for a set of fields on a table.
65
- *
66
- * Called by MigrationRunner AFTER all tables in a migration have been
67
- * created — this guarantees all referenced tables exist.
68
- *
69
- * All FK columns for a given table are batched into a single ALTER TABLE
70
- * statement, not one per column.
71
- *
72
- * @param {import('knex').Knex} db
73
- * @param {string} table — table name
74
- * @param {object} fields — { columnName: normalisedDef, ... }
75
- */
76
45
  async function attachFKConstraints(db, table, fields) {
77
46
  const fkEntries = Object.entries(fields).filter(([, def]) => def.references);
78
-
79
47
  if (fkEntries.length === 0) return;
80
48
 
81
49
  await db.schema.alterTable(table, (t) => {
@@ -91,22 +59,11 @@ async function attachFKConstraints(db, table, fields) {
91
59
 
92
60
  // ─── Internal helpers ─────────────────────────────────────────────────────────
93
61
 
94
- /**
95
- * Build a knex column builder for a given field type.
96
- * Returns null for 'id' fields (handled by t.increments which returns void).
97
- *
98
- * @param {object} t
99
- * @param {string} name
100
- * @param {object} def
101
- * @param {object} [opts]
102
- * @param {boolean} [opts.forAlter] — if true, skip t.increments (can't alter PK)
103
- * @returns {object|null} knex column builder
104
- */
105
62
  function _buildColumn(t, name, def, opts = {}) {
106
63
  switch (def.type) {
107
64
  case 'id':
108
65
  if (!opts.forAlter) t.increments(name).primary();
109
- return null; // increments() doesn't return a chainable column builder
66
+ return null;
110
67
 
111
68
  case 'string':
112
69
  case 'email':
@@ -119,14 +76,10 @@ function _buildColumn(t, name, def, opts = {}) {
119
76
  return t.text(name);
120
77
 
121
78
  case 'integer':
122
- return def.unsigned
123
- ? t.integer(name).unsigned()
124
- : t.integer(name);
79
+ return def.unsigned ? t.integer(name).unsigned() : t.integer(name);
125
80
 
126
81
  case 'bigInteger':
127
- return def.unsigned
128
- ? t.bigInteger(name).unsigned()
129
- : t.bigInteger(name);
82
+ return def.unsigned ? t.bigInteger(name).unsigned() : t.bigInteger(name);
130
83
 
131
84
  case 'float':
132
85
  return t.float(name);
@@ -146,43 +99,36 @@ function _buildColumn(t, name, def, opts = {}) {
146
99
  case 'timestamp':
147
100
  return t.timestamp(name, { useTz: false });
148
101
 
149
- case 'enum':
102
+ case 'enum': {
103
+ const client = t.client?.config?.client || '';
104
+ if (client.includes('pg') || client.includes('postgres')) {
105
+ // Store as text — CHECK constraint added separately in applyColumn
106
+ return t.text(name);
107
+ }
150
108
  return t.enu(name, def.enumValues || []);
109
+ }
151
110
 
152
111
  case 'uuid':
153
112
  return t.uuid(name);
154
113
 
155
114
  default:
156
- return t.string(name); // safe fallback
115
+ return t.string(name);
157
116
  }
158
117
  }
159
118
 
160
- /**
161
- * Apply nullability, uniqueness, default, and FK constraint modifiers
162
- * to an already-built knex column builder.
163
- *
164
- * @param {object} col — knex column builder
165
- * @param {object} def — normalised field def
166
- * @param {object} [opts]
167
- * @param {boolean} [opts.skipFK] — skip FK constraint (used by alterColumn)
168
- */
169
119
  function _applyModifiers(col, def, opts = {}) {
170
- // Nullability
171
120
  if (def.nullable) {
172
121
  col.nullable();
173
122
  } else if (def.type !== 'id') {
174
123
  col.notNullable();
175
124
  }
176
125
 
177
- // Uniqueness
178
126
  if (def.unique) col.unique();
179
127
 
180
- // Default value
181
128
  if (def.default !== null && def.default !== undefined) {
182
129
  col.defaultTo(def.default);
183
130
  }
184
131
 
185
- // Inline FK constraint — skipped when deferring to attachFKConstraints()
186
132
  if (!opts.skipFK && def.references) {
187
133
  const ref = def.references;
188
134
  col
@@ -192,4 +138,4 @@ function _applyModifiers(col, def, opts = {}) {
192
138
  }
193
139
  }
194
140
 
195
- module.exports = { applyColumn, alterColumn, attachFKConstraints };
141
+ module.exports = { applyColumn, alterColumn, attachFKConstraints };
@@ -53,7 +53,7 @@ class AddField extends BaseOperation {
53
53
  await this._safeBackfill(db, def);
54
54
  } else {
55
55
  await db.schema.table(this.table, (t) => {
56
- applyColumn(t, this.column, def);
56
+ applyColumn(t, this.column, def, this.table);
57
57
  });
58
58
  }
59
59
  }
@@ -94,7 +94,7 @@ class AddField extends BaseOperation {
94
94
 
95
95
  // Step 1: add as nullable
96
96
  await db.schema.table(this.table, (t) => {
97
- applyColumn(t, this.column, { ...def, nullable: true, default: null });
97
+ applyColumn(t, this.column, { ...def, nullable: true, default: null }, this.table);
98
98
  });
99
99
 
100
100
  // Step 2: backfill
@@ -115,7 +115,7 @@ class AddField extends BaseOperation {
115
115
 
116
116
  // Step 3: tighten to NOT NULL
117
117
  await db.schema.alterTable(this.table, (t) => {
118
- alterColumn(t, this.column, { ...def, nullable: false });
118
+ alterColumn(t, this.column, { ...def, nullable: false }, this.table);
119
119
  });
120
120
  }
121
121
  }
@@ -148,7 +148,7 @@ class RemoveField extends BaseOperation {
148
148
 
149
149
  async down(db) {
150
150
  await db.schema.table(this.table, (t) => {
151
- applyColumn(t, this.column, normaliseField(this.field));
151
+ applyColumn(t, this.column, normaliseField(this.field), this.table);
152
152
  });
153
153
  }
154
154
 
@@ -186,13 +186,13 @@ class AlterField extends BaseOperation {
186
186
 
187
187
  async up(db) {
188
188
  await db.schema.alterTable(this.table, (t) => {
189
- alterColumn(t, this.column, normaliseField(this.field));
189
+ alterColumn(t, this.column, normaliseField(this.field), this.table);
190
190
  });
191
191
  }
192
192
 
193
193
  async down(db) {
194
194
  await db.schema.alterTable(this.table, (t) => {
195
- alterColumn(t, this.column, normaliseField(this.previousField));
195
+ alterColumn(t, this.column, normaliseField(this.previousField), this.table);
196
196
  });
197
197
  }
198
198
 
@@ -48,7 +48,7 @@ class CreateModel extends BaseOperation {
48
48
  async up(db) {
49
49
  await db.schema.createTable(this.table, (t) => {
50
50
  for (const [name, def] of Object.entries(this.fields)) {
51
- applyColumn(t, name, normaliseField(def));
51
+ applyColumn(t, name, normaliseField(def), this.table);
52
52
  }
53
53
  });
54
54
  await this._applyIndexes(db);
@@ -61,7 +61,7 @@ class CreateModel extends BaseOperation {
61
61
  async upWithoutFKs(db) {
62
62
  await db.schema.createTable(this.table, (t) => {
63
63
  for (const [name, def] of Object.entries(this.fields)) {
64
- applyColumn(t, name, { ...normaliseField(def), references: null });
64
+ applyColumn(t, name, { ...normaliseField(def), references: null }, this.table);
65
65
  }
66
66
  });
67
67
  await this._applyIndexes(db);
@@ -134,7 +134,7 @@ class DeleteModel extends BaseOperation {
134
134
  async down(db) {
135
135
  await db.schema.createTable(this.table, (t) => {
136
136
  for (const [name, def] of Object.entries(this.fields)) {
137
- applyColumn(t, name, normaliseField(def));
137
+ applyColumn(t, name, normaliseField(def), this.table);
138
138
  }
139
139
  });
140
140
  }