millas 0.2.19 → 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 +1 -1
- package/src/admin/QueryEngine.js +17 -13
- package/src/cli.js +3 -0
- package/src/commands/migrate.js +34 -2
- package/src/container/AppInitializer.js +43 -0
- package/src/core/db.js +9 -8
- package/src/orm/drivers/DatabaseManager.js +12 -0
- package/src/orm/fields/index.js +18 -11
- package/src/orm/migration/Makemigrations.js +34 -29
- package/src/orm/migration/MigrationWriter.js +117 -13
- package/src/orm/migration/ModelInspector.js +4 -0
- package/src/orm/migration/ModelScanner.js +12 -6
- package/src/orm/migration/ProjectState.js +41 -5
- package/src/orm/migration/operations/column.js +45 -95
- package/src/orm/migration/operations/fields.js +6 -6
- package/src/orm/migration/operations/index.js +7 -24
- package/src/orm/migration/operations/indexes.js +197 -0
- package/src/orm/migration/operations/models.js +35 -9
- package/src/orm/migration/operations/registry.js +24 -3
- package/src/orm/model/Model.js +315 -72
- package/src/orm/query/F.js +98 -0
- package/src/orm/query/LookupParser.js +316 -157
- package/src/orm/query/QueryBuilder.js +178 -8
- package/src/providers/DatabaseServiceProvider.js +2 -2
|
@@ -311,9 +311,20 @@ class QueryBuilder {
|
|
|
311
311
|
return instance;
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
-
/**
|
|
315
|
-
|
|
316
|
-
|
|
314
|
+
/**
|
|
315
|
+
* Find by primary key — allows chaining with .with().
|
|
316
|
+
* Post.with('author').find(1)
|
|
317
|
+
*/
|
|
318
|
+
async find(id) {
|
|
319
|
+
return this.where(this._model.primaryKey, id).first();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Find by primary key or throw 404.
|
|
324
|
+
* Post.with('author').findOrFail(1)
|
|
325
|
+
*/
|
|
326
|
+
async findOrFail(id, message) {
|
|
327
|
+
const result = await this.find(id);
|
|
317
328
|
if (!result) {
|
|
318
329
|
const HttpError = require('../../errors/HttpError');
|
|
319
330
|
throw new HttpError(404, message || `${this._model.name} not found`);
|
|
@@ -323,7 +334,11 @@ class QueryBuilder {
|
|
|
323
334
|
|
|
324
335
|
/** Count matching rows. */
|
|
325
336
|
async count(column = '*') {
|
|
326
|
-
const result = await this._query.clone()
|
|
337
|
+
const result = await this._query.clone()
|
|
338
|
+
.clearSelect()
|
|
339
|
+
.clearOrder()
|
|
340
|
+
.count(`${column} as count`)
|
|
341
|
+
.first();
|
|
327
342
|
return Number(result?.count ?? 0);
|
|
328
343
|
}
|
|
329
344
|
|
|
@@ -344,7 +359,11 @@ class QueryBuilder {
|
|
|
344
359
|
*/
|
|
345
360
|
async paginate(page = 1, perPage = 15) {
|
|
346
361
|
const offset = (page - 1) * perPage;
|
|
347
|
-
const total = await this._query.clone()
|
|
362
|
+
const total = await this._query.clone()
|
|
363
|
+
.clearSelect()
|
|
364
|
+
.clearOrder()
|
|
365
|
+
.count('* as count')
|
|
366
|
+
.first()
|
|
348
367
|
.then(r => Number(r?.count ?? 0));
|
|
349
368
|
|
|
350
369
|
const rows = await this._query.clone().limit(perPage).offset(offset);
|
|
@@ -363,7 +382,17 @@ class QueryBuilder {
|
|
|
363
382
|
|
|
364
383
|
/** Update matching rows. */
|
|
365
384
|
async update(data) {
|
|
366
|
-
|
|
385
|
+
const F = require('./F');
|
|
386
|
+
const extra = this._model._updatedAtPayload?.() ?? {};
|
|
387
|
+
const payload = {};
|
|
388
|
+
for (const [key, val] of Object.entries({ ...data, ...extra })) {
|
|
389
|
+
if (val instanceof F) {
|
|
390
|
+
payload[key] = val.toKnex(this._query.client);
|
|
391
|
+
} else {
|
|
392
|
+
payload[key] = val;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return this._query.update(payload);
|
|
367
396
|
}
|
|
368
397
|
|
|
369
398
|
/** Delete matching rows. */
|
|
@@ -371,7 +400,148 @@ class QueryBuilder {
|
|
|
371
400
|
return this._query.delete();
|
|
372
401
|
}
|
|
373
402
|
|
|
374
|
-
/**
|
|
403
|
+
/**
|
|
404
|
+
* Get exactly one result — raises if 0 or more than 1 found.
|
|
405
|
+
* Matches Django's QuerySet.get()
|
|
406
|
+
*/
|
|
407
|
+
async get_one(...args) {
|
|
408
|
+
if (args.length) this.where(...args);
|
|
409
|
+
this._query = this._query.limit(2);
|
|
410
|
+
const rows = await this._query;
|
|
411
|
+
if (rows.length === 0) {
|
|
412
|
+
const HttpError = require('../../errors/HttpError');
|
|
413
|
+
throw new HttpError(404, `${this._model.name} matching query does not exist.`);
|
|
414
|
+
}
|
|
415
|
+
if (rows.length > 1) {
|
|
416
|
+
throw new Error(`get() returned more than one ${this._model.name} — it returned ${rows.length}!`);
|
|
417
|
+
}
|
|
418
|
+
const instance = this._model._hydrate(rows[0]);
|
|
419
|
+
if (this._withs.length) await this._eagerLoad([instance]);
|
|
420
|
+
return instance;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Return last matching row (reverse of first()).
|
|
425
|
+
* Post.orderBy('created_at').last()
|
|
426
|
+
*/
|
|
427
|
+
async last() {
|
|
428
|
+
const rows = await this._query;
|
|
429
|
+
if (!rows.length) return null;
|
|
430
|
+
const instance = this._model._hydrate(rows[rows.length - 1]);
|
|
431
|
+
if (this._withs.length) await this._eagerLoad([instance]);
|
|
432
|
+
return instance;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Return empty result set — useful for conditional queries.
|
|
437
|
+
* Post.none().get() // []
|
|
438
|
+
*/
|
|
439
|
+
none() {
|
|
440
|
+
this._query = this._query.whereRaw('1 = 0');
|
|
441
|
+
return this;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Lock selected rows for update (SELECT FOR UPDATE).
|
|
446
|
+
* Postgres and MySQL only.
|
|
447
|
+
* Post.where('status', 'pending').selectForUpdate().first()
|
|
448
|
+
*/
|
|
449
|
+
selectForUpdate(options = {}) {
|
|
450
|
+
const client = this._query.client?.config?.client || '';
|
|
451
|
+
if (client.includes('sqlite')) {
|
|
452
|
+
// SQLite doesn't support SELECT FOR UPDATE — silently skip
|
|
453
|
+
return this;
|
|
454
|
+
}
|
|
455
|
+
if (options.skipLocked) {
|
|
456
|
+
this._query = this._query.forUpdate().skipLocked();
|
|
457
|
+
} else if (options.noWait) {
|
|
458
|
+
this._query = this._query.forUpdate().noWait();
|
|
459
|
+
} else {
|
|
460
|
+
this._query = this._query.forUpdate();
|
|
461
|
+
}
|
|
462
|
+
return this;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Return a dict mapping pk → instance.
|
|
467
|
+
* Matches Django's QuerySet.in_bulk()
|
|
468
|
+
* Post.inBulk([1, 2, 3]) // { 1: Post, 2: Post, 3: Post }
|
|
469
|
+
* Post.inBulk() // all rows as dict
|
|
470
|
+
*/
|
|
471
|
+
async inBulk(ids = null, fieldName = null) {
|
|
472
|
+
const pk = fieldName || this._model.primaryKey;
|
|
473
|
+
if (ids !== null) {
|
|
474
|
+
if (ids.length === 0) return {};
|
|
475
|
+
this._query = this._query.whereIn(pk, ids);
|
|
476
|
+
}
|
|
477
|
+
const rows = await this._query;
|
|
478
|
+
const result = {};
|
|
479
|
+
for (const row of rows) {
|
|
480
|
+
const instance = this._model._hydrate(row);
|
|
481
|
+
result[instance[pk]] = instance;
|
|
482
|
+
}
|
|
483
|
+
return result;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Reverse the current ordering.
|
|
488
|
+
* Matches Django's QuerySet.reverse()
|
|
489
|
+
*/
|
|
490
|
+
reverse() {
|
|
491
|
+
// Wrap current query as subquery and reverse order
|
|
492
|
+
// Simple implementation: toggle asc/desc on existing order
|
|
493
|
+
this._reversed = true;
|
|
494
|
+
return this;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Return query plan (EXPLAIN).
|
|
499
|
+
* Matches Django's QuerySet.explain()
|
|
500
|
+
*/
|
|
501
|
+
async explain() {
|
|
502
|
+
const client = this._query.client?.config?.client || '';
|
|
503
|
+
const sql = this._query.toSQL();
|
|
504
|
+
const prefix = client.includes('pg') ? 'EXPLAIN ANALYZE ' : 'EXPLAIN ';
|
|
505
|
+
const db = require('../drivers/DatabaseManager').connection(this._model.connection || null);
|
|
506
|
+
const result = await db.raw(prefix + sql.sql, sql.bindings);
|
|
507
|
+
return result.rows ?? result[0] ?? result;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Use a specific DB connection for this query.
|
|
512
|
+
* Matches Django's QuerySet.using()
|
|
513
|
+
*/
|
|
514
|
+
using(connectionName) {
|
|
515
|
+
const DatabaseManager = require('../drivers/DatabaseManager');
|
|
516
|
+
const db = DatabaseManager.connection(connectionName);
|
|
517
|
+
this._query = db(this._model.table);
|
|
518
|
+
return this;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Return the raw SQL string with bindings substituted — for debugging.
|
|
523
|
+
* Matches Django's str(queryset.query) behaviour.
|
|
524
|
+
*
|
|
525
|
+
* console.log(Post.where('published', true).orderBy('created_at').sql())
|
|
526
|
+
* // → select * from `posts` where `published` = true order by `created_at` asc
|
|
527
|
+
*
|
|
528
|
+
* // Also available as toSQL() for the raw { sql, bindings } object
|
|
529
|
+
* Post.where('published', true).toSQL()
|
|
530
|
+
* // → { sql: 'select * from `posts` where `published` = ?', bindings: [true] }
|
|
531
|
+
*/
|
|
532
|
+
sql() {
|
|
533
|
+
const { sql, bindings } = this._query.toSQL();
|
|
534
|
+
// Substitute bindings into the SQL string for readability
|
|
535
|
+
let result = sql;
|
|
536
|
+
for (const binding of bindings) {
|
|
537
|
+
const val = binding === null ? 'NULL'
|
|
538
|
+
: typeof binding === 'string' ? `'${binding}'`
|
|
539
|
+
: String(binding);
|
|
540
|
+
result = result.replace('?', val);
|
|
541
|
+
}
|
|
542
|
+
return result;
|
|
543
|
+
}
|
|
544
|
+
|
|
375
545
|
toSQL() {
|
|
376
546
|
return this._query.toSQL();
|
|
377
547
|
}
|
|
@@ -381,7 +551,7 @@ class QueryBuilder {
|
|
|
381
551
|
async _eagerLoad(instances) {
|
|
382
552
|
if (!instances.length) return;
|
|
383
553
|
|
|
384
|
-
const relations = this._model.
|
|
554
|
+
const relations = this._model._effectiveRelations();
|
|
385
555
|
|
|
386
556
|
for (const { name, constraint, aggregate, aggregateColumn } of this._withs) {
|
|
387
557
|
|
|
@@ -26,8 +26,8 @@ class DatabaseServiceProvider extends ServiceProvider {
|
|
|
26
26
|
let dbConfig;
|
|
27
27
|
try {
|
|
28
28
|
dbConfig = require(basePath + '/config/database');
|
|
29
|
-
} catch {
|
|
30
|
-
|
|
29
|
+
} catch (err) {
|
|
30
|
+
if (err.code !== 'MODULE_NOT_FOUND') throw err;
|
|
31
31
|
dbConfig = {
|
|
32
32
|
default: 'sqlite',
|
|
33
33
|
connections: {
|