@getstrata/core 0.5.97 → 0.5.99
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/CHANGELOG.md +30 -0
- package/dist/core/contracts/container.d.ts +2 -0
- package/dist/core/database/baseRepository.d.ts +3 -1
- package/dist/core/database/factory.d.ts +47 -5
- package/dist/core/database/index.d.ts +3 -1
- package/dist/core/database/inflection.d.ts +4 -0
- package/dist/core/database/model.d.ts +88 -4
- package/dist/core/database/relationQuery.d.ts +172 -0
- package/dist/core/database/repositoryQuery.d.ts +9 -1
- package/dist/core/database/whereBuilder.d.ts +9 -1
- package/dist/core/events/eventBus.d.ts +2 -0
- package/dist/core/http/resources.d.ts +21 -1
- package/dist/entries/contracts/container.js +6 -0
- package/dist/entries/database/factory.js +159 -6
- package/dist/entries/database/model.js +1066 -23
- package/dist/entries/database/query.js +10 -1
- package/dist/entries/database/repositoryQuery.js +55 -3
- package/dist/entries/database/schema.js +10 -1
- package/dist/entries/http/resources.js +68 -1
- package/dist/framework/public-api.d.ts +4 -2
- package/dist/index.js +1374 -25
- package/package.json +1 -1
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
// ../../src/core/database/model.ts
|
|
3
3
|
import { NotFoundError } from "@getstrata/core/errors/http";
|
|
4
4
|
|
|
5
|
+
// ../../src/core/database/inflection.ts
|
|
6
|
+
function singularize(word) {
|
|
7
|
+
if (word.endsWith("ies") && word.length > 3) {
|
|
8
|
+
return `${word.slice(0, -3)}y`;
|
|
9
|
+
}
|
|
10
|
+
if (/(ses|xes|zes|ches|shes)$/i.test(word)) {
|
|
11
|
+
return word.slice(0, -2);
|
|
12
|
+
}
|
|
13
|
+
if (word.endsWith("s") && !word.endsWith("ss")) {
|
|
14
|
+
return word.slice(0, -1);
|
|
15
|
+
}
|
|
16
|
+
return word;
|
|
17
|
+
}
|
|
18
|
+
function foreignKeyFromTable(tableName) {
|
|
19
|
+
return `${singularize(tableName)}_id`;
|
|
20
|
+
}
|
|
21
|
+
function pivotTableName(leftTable, rightTable) {
|
|
22
|
+
return [singularize(leftTable), singularize(rightTable)].sort().join("_");
|
|
23
|
+
}
|
|
24
|
+
|
|
5
25
|
// ../../src/core/database/query.ts
|
|
6
26
|
function quoteIdentifier(identifier) {
|
|
7
27
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
|
|
@@ -77,7 +97,7 @@ function buildOperatorClauses(column, operator, params) {
|
|
|
77
97
|
clauses.push(`${column} <= ${pushParam(params, operator.lte)}`);
|
|
78
98
|
}
|
|
79
99
|
if (operator.ilike !== undefined) {
|
|
80
|
-
clauses.push(`${column} ILIKE ${pushParam(params,
|
|
100
|
+
clauses.push(`${column} ILIKE ${pushParam(params, operator.ilike)}`);
|
|
81
101
|
}
|
|
82
102
|
if (operator.tsMatch !== undefined) {
|
|
83
103
|
clauses.push(`${column} @@ plainto_tsquery('english', ${pushParam(params, operator.tsMatch)})`);
|
|
@@ -115,7 +135,16 @@ function buildWhereClause(tableName, where = {}) {
|
|
|
115
135
|
params
|
|
116
136
|
};
|
|
117
137
|
}
|
|
138
|
+
function remapExistsSql(sql, existsParams, params) {
|
|
139
|
+
const offset = params.length;
|
|
140
|
+
params.push(...existsParams);
|
|
141
|
+
return sql.replace(/\$(\d+)/g, (_match, index) => `$${offset + Number(index)}`);
|
|
142
|
+
}
|
|
118
143
|
function buildWhereNodeClause(tableName, node, params) {
|
|
144
|
+
if ("exists" in node) {
|
|
145
|
+
const body = remapExistsSql(node.exists.sql, node.exists.params, params);
|
|
146
|
+
return `${node.exists.not ? "NOT " : ""}EXISTS (${body})`;
|
|
147
|
+
}
|
|
119
148
|
if ("where" in node) {
|
|
120
149
|
return appendWhereParts(tableName, node.where, params);
|
|
121
150
|
}
|
|
@@ -416,6 +445,518 @@ function buildDeleteByIdQuery(table, id) {
|
|
|
416
445
|
};
|
|
417
446
|
}
|
|
418
447
|
|
|
448
|
+
// ../../src/core/database/relationQuery.ts
|
|
449
|
+
function asWhere(where) {
|
|
450
|
+
return where;
|
|
451
|
+
}
|
|
452
|
+
function ownerId(owner, ownerKey) {
|
|
453
|
+
if (typeof owner.get === "function") {
|
|
454
|
+
return owner.get(ownerKey);
|
|
455
|
+
}
|
|
456
|
+
if (owner && typeof owner === "object" && "id" in owner && owner.id !== undefined) {
|
|
457
|
+
return owner.id;
|
|
458
|
+
}
|
|
459
|
+
throw new Error("belongsTo.associate() requires a related model or { id }.");
|
|
460
|
+
}
|
|
461
|
+
function thenGet(get, onfulfilled, onrejected) {
|
|
462
|
+
return get().then(onfulfilled ?? undefined, onrejected ?? undefined);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
class HasManyRelationQuery {
|
|
466
|
+
parent;
|
|
467
|
+
related;
|
|
468
|
+
relation;
|
|
469
|
+
kind = "hasMany";
|
|
470
|
+
extraWhere = {};
|
|
471
|
+
extraOptions = {};
|
|
472
|
+
constructor(parent, related, relation) {
|
|
473
|
+
this.parent = parent;
|
|
474
|
+
this.related = related;
|
|
475
|
+
this.relation = relation;
|
|
476
|
+
}
|
|
477
|
+
where(where) {
|
|
478
|
+
this.extraWhere = { ...this.extraWhere, ...where };
|
|
479
|
+
return this;
|
|
480
|
+
}
|
|
481
|
+
orderBy(orderBy) {
|
|
482
|
+
this.extraOptions = { ...this.extraOptions, orderBy };
|
|
483
|
+
return this;
|
|
484
|
+
}
|
|
485
|
+
limit(limit) {
|
|
486
|
+
this.extraOptions = { ...this.extraOptions, limit };
|
|
487
|
+
return this;
|
|
488
|
+
}
|
|
489
|
+
applyEagerLoad(query, alias) {
|
|
490
|
+
query.withHasMany(alias, this.relation, this.related.repository(), this.extraOptions);
|
|
491
|
+
}
|
|
492
|
+
hydrateEager(row, alias) {
|
|
493
|
+
const value = row[alias] ?? [];
|
|
494
|
+
const rows = Array.isArray(value) ? value : [];
|
|
495
|
+
return rows.map((item) => this.related.newFromRecord(item));
|
|
496
|
+
}
|
|
497
|
+
toExistsClause(parentTable) {
|
|
498
|
+
const childTable = this.related.repository().getTable().name;
|
|
499
|
+
const extra = buildAdvancedWhereClause(childTable, this.extraWhere, [], []);
|
|
500
|
+
const extraSql = extra.clause.replace(/^ WHERE /, "");
|
|
501
|
+
const sql = `SELECT 1 FROM ${quoteIdentifier(childTable)} WHERE ${qualifyColumn(childTable, this.relation.foreignKey)} = ${qualifyColumn(parentTable, this.relation.localKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
|
|
502
|
+
return { sql, params: extra.params };
|
|
503
|
+
}
|
|
504
|
+
scopedQuery() {
|
|
505
|
+
const repository = this.related.repository().withConnection(this.parent.getRepository().getConnection());
|
|
506
|
+
let query = repository.query(asWhere({
|
|
507
|
+
[this.relation.foreignKey]: this.parent.get(this.relation.localKey),
|
|
508
|
+
...this.extraWhere
|
|
509
|
+
}));
|
|
510
|
+
if (this.extraOptions.orderBy) {
|
|
511
|
+
query = query.orderBy(this.extraOptions.orderBy);
|
|
512
|
+
}
|
|
513
|
+
if (this.extraOptions.limit !== undefined) {
|
|
514
|
+
query = query.limit(this.extraOptions.limit);
|
|
515
|
+
}
|
|
516
|
+
return query;
|
|
517
|
+
}
|
|
518
|
+
async get() {
|
|
519
|
+
const rows = await this.scopedQuery().get();
|
|
520
|
+
return rows.map((row) => this.related.newFromRecord(row));
|
|
521
|
+
}
|
|
522
|
+
async first() {
|
|
523
|
+
const rows = await this.limit(1).get();
|
|
524
|
+
return rows[0] ?? null;
|
|
525
|
+
}
|
|
526
|
+
async count() {
|
|
527
|
+
return this.scopedQuery().count();
|
|
528
|
+
}
|
|
529
|
+
then(onfulfilled, onrejected) {
|
|
530
|
+
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
531
|
+
}
|
|
532
|
+
async create(attributes = {}) {
|
|
533
|
+
return this.related.create(attributes, {
|
|
534
|
+
[this.relation.foreignKey]: this.parent.get(this.relation.localKey)
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
async save(related) {
|
|
538
|
+
const forced = {
|
|
539
|
+
[this.relation.foreignKey]: this.parent.get(this.relation.localKey)
|
|
540
|
+
};
|
|
541
|
+
const savable = related;
|
|
542
|
+
if (typeof savable.save === "function") {
|
|
543
|
+
savable.mergeAttributes?.(forced);
|
|
544
|
+
await savable.save();
|
|
545
|
+
return related;
|
|
546
|
+
}
|
|
547
|
+
return this.create(related);
|
|
548
|
+
}
|
|
549
|
+
async createMany(records) {
|
|
550
|
+
const created = [];
|
|
551
|
+
for (const attributes of records) {
|
|
552
|
+
created.push(await this.create(attributes));
|
|
553
|
+
}
|
|
554
|
+
return created;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
class HasOneRelationQuery {
|
|
559
|
+
relation;
|
|
560
|
+
kind = "hasOne";
|
|
561
|
+
inner;
|
|
562
|
+
constructor(parent, related, relation) {
|
|
563
|
+
this.relation = relation;
|
|
564
|
+
this.inner = new HasManyRelationQuery(parent, related, {
|
|
565
|
+
type: "hasMany",
|
|
566
|
+
name: relation.name,
|
|
567
|
+
localKey: relation.localKey,
|
|
568
|
+
foreignKey: relation.foreignKey
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
where(where) {
|
|
572
|
+
this.inner.where(where);
|
|
573
|
+
return this;
|
|
574
|
+
}
|
|
575
|
+
orderBy(orderBy) {
|
|
576
|
+
this.inner.orderBy(orderBy);
|
|
577
|
+
return this;
|
|
578
|
+
}
|
|
579
|
+
applyEagerLoad(query, alias) {
|
|
580
|
+
this.inner.limit(1).applyEagerLoad(query, alias);
|
|
581
|
+
}
|
|
582
|
+
hydrateEager(row, alias) {
|
|
583
|
+
const hydrated = this.inner.hydrateEager(row, alias);
|
|
584
|
+
return hydrated[0];
|
|
585
|
+
}
|
|
586
|
+
toExistsClause(parentTable) {
|
|
587
|
+
return this.inner.toExistsClause(parentTable);
|
|
588
|
+
}
|
|
589
|
+
async get() {
|
|
590
|
+
return this.inner.limit(1).first();
|
|
591
|
+
}
|
|
592
|
+
async first() {
|
|
593
|
+
return this.get();
|
|
594
|
+
}
|
|
595
|
+
async count() {
|
|
596
|
+
return this.inner.count();
|
|
597
|
+
}
|
|
598
|
+
then(onfulfilled, onrejected) {
|
|
599
|
+
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
600
|
+
}
|
|
601
|
+
async create(attributes = {}) {
|
|
602
|
+
return this.inner.create(attributes);
|
|
603
|
+
}
|
|
604
|
+
async save(related) {
|
|
605
|
+
return this.inner.save(related);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
class BelongsToRelationQuery {
|
|
610
|
+
parent;
|
|
611
|
+
related;
|
|
612
|
+
relation;
|
|
613
|
+
kind = "belongsTo";
|
|
614
|
+
extraWhere = {};
|
|
615
|
+
extraOptions = {};
|
|
616
|
+
constructor(parent, related, relation) {
|
|
617
|
+
this.parent = parent;
|
|
618
|
+
this.related = related;
|
|
619
|
+
this.relation = relation;
|
|
620
|
+
}
|
|
621
|
+
where(where) {
|
|
622
|
+
this.extraWhere = { ...this.extraWhere, ...where };
|
|
623
|
+
return this;
|
|
624
|
+
}
|
|
625
|
+
orderBy(orderBy) {
|
|
626
|
+
this.extraOptions = { ...this.extraOptions, orderBy };
|
|
627
|
+
return this;
|
|
628
|
+
}
|
|
629
|
+
applyEagerLoad(query, alias) {
|
|
630
|
+
query.withBelongsTo(alias, this.relation, this.related.repository(), this.extraOptions);
|
|
631
|
+
}
|
|
632
|
+
hydrateEager(row, alias) {
|
|
633
|
+
const value = row[alias];
|
|
634
|
+
return value ? this.related.newFromRecord(value) : value;
|
|
635
|
+
}
|
|
636
|
+
toExistsClause(parentTable) {
|
|
637
|
+
const relatedTable = this.related.repository().getTable().name;
|
|
638
|
+
const extra = buildAdvancedWhereClause(relatedTable, this.extraWhere, [], []);
|
|
639
|
+
const extraSql = extra.clause.replace(/^ WHERE /, "");
|
|
640
|
+
const sql = `SELECT 1 FROM ${quoteIdentifier(relatedTable)} WHERE ${qualifyColumn(relatedTable, this.relation.ownerKey)} = ${qualifyColumn(parentTable, this.relation.foreignKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
|
|
641
|
+
return { sql, params: extra.params };
|
|
642
|
+
}
|
|
643
|
+
async get() {
|
|
644
|
+
const foreign = this.parent.get(this.relation.foreignKey);
|
|
645
|
+
if (foreign === null || foreign === undefined) {
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
const repository = this.related.repository().withConnection(this.parent.getRepository().getConnection());
|
|
649
|
+
let query = repository.query(asWhere({ [this.relation.ownerKey]: foreign, ...this.extraWhere }));
|
|
650
|
+
if (this.extraOptions.orderBy) {
|
|
651
|
+
query = query.orderBy(this.extraOptions.orderBy);
|
|
652
|
+
}
|
|
653
|
+
const row = await query.first();
|
|
654
|
+
return row ? this.related.newFromRecord(row) : null;
|
|
655
|
+
}
|
|
656
|
+
async first() {
|
|
657
|
+
return this.get();
|
|
658
|
+
}
|
|
659
|
+
then(onfulfilled, onrejected) {
|
|
660
|
+
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
661
|
+
}
|
|
662
|
+
async associate(owner) {
|
|
663
|
+
await this.parent.getRepository().updateById(this.parent.id, {
|
|
664
|
+
[this.relation.foreignKey]: ownerId(owner, this.relation.ownerKey)
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
async dissociate() {
|
|
668
|
+
await this.parent.getRepository().updateById(this.parent.id, {
|
|
669
|
+
[this.relation.foreignKey]: null
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
class BelongsToManyRelationQuery {
|
|
675
|
+
parent;
|
|
676
|
+
related;
|
|
677
|
+
relation;
|
|
678
|
+
kind = "belongsToMany";
|
|
679
|
+
extraWhere = {};
|
|
680
|
+
extraOptions = {};
|
|
681
|
+
pivotValues = {};
|
|
682
|
+
constructor(parent, related, relation) {
|
|
683
|
+
this.parent = parent;
|
|
684
|
+
this.related = related;
|
|
685
|
+
this.relation = relation;
|
|
686
|
+
}
|
|
687
|
+
where(where) {
|
|
688
|
+
this.extraWhere = { ...this.extraWhere, ...where };
|
|
689
|
+
return this;
|
|
690
|
+
}
|
|
691
|
+
orderBy(orderBy) {
|
|
692
|
+
this.extraOptions = { ...this.extraOptions, orderBy };
|
|
693
|
+
return this;
|
|
694
|
+
}
|
|
695
|
+
applyEagerLoad(query, alias) {
|
|
696
|
+
query.withBelongsToMany(alias, this.relation, this.related.repository(), this.extraOptions);
|
|
697
|
+
}
|
|
698
|
+
withPivotValues(values) {
|
|
699
|
+
this.pivotValues = { ...this.pivotValues, ...values };
|
|
700
|
+
return this;
|
|
701
|
+
}
|
|
702
|
+
hydrateEager(row, alias) {
|
|
703
|
+
const value = row[alias] ?? [];
|
|
704
|
+
const rows = Array.isArray(value) ? value : [];
|
|
705
|
+
return rows.map((item) => this.related.newFromRecord(item));
|
|
706
|
+
}
|
|
707
|
+
toExistsClause(parentTable) {
|
|
708
|
+
const relatedTable = this.related.repository().getTable().name;
|
|
709
|
+
const extra = buildAdvancedWhereClause(relatedTable, this.extraWhere, [], []);
|
|
710
|
+
const extraSql = extra.clause.replace(/^ WHERE /, "");
|
|
711
|
+
const sql = `SELECT 1 FROM ${quoteIdentifier(relatedTable)} INNER JOIN ${quoteIdentifier(this.relation.pivotTable)} ON ${qualifyColumn(this.relation.pivotTable, this.relation.relatedPivotKey)} = ${qualifyColumn(relatedTable, this.relation.relatedKey)} WHERE ${qualifyColumn(this.relation.pivotTable, this.relation.foreignPivotKey)} = ${qualifyColumn(parentTable, this.relation.parentKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
|
|
712
|
+
return { sql, params: extra.params };
|
|
713
|
+
}
|
|
714
|
+
connection() {
|
|
715
|
+
return this.parent.getRepository().getConnection();
|
|
716
|
+
}
|
|
717
|
+
async get() {
|
|
718
|
+
const parentId = this.parent.get(this.relation.parentKey);
|
|
719
|
+
const pivotRows = await this.connection().unsafe(`SELECT * FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1`, [parentId]);
|
|
720
|
+
if (pivotRows.length === 0) {
|
|
721
|
+
return [];
|
|
722
|
+
}
|
|
723
|
+
const relatedIds = [
|
|
724
|
+
...new Set(pivotRows.map((row) => row[this.relation.relatedPivotKey]))
|
|
725
|
+
];
|
|
726
|
+
const repository = this.related.repository().withConnection(this.connection());
|
|
727
|
+
const rows = await repository.findAll({
|
|
728
|
+
...this.extraOptions,
|
|
729
|
+
where: asWhere({
|
|
730
|
+
[this.relation.relatedKey]: relatedIds,
|
|
731
|
+
...this.extraWhere
|
|
732
|
+
})
|
|
733
|
+
});
|
|
734
|
+
return rows.map((row) => this.related.newFromRecord(row));
|
|
735
|
+
}
|
|
736
|
+
async first() {
|
|
737
|
+
const rows = await this.get();
|
|
738
|
+
return rows[0] ?? null;
|
|
739
|
+
}
|
|
740
|
+
async count() {
|
|
741
|
+
const parentId = this.parent.get(this.relation.parentKey);
|
|
742
|
+
const rows = await this.connection().unsafe(`SELECT COUNT(*) AS count FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1`, [parentId]);
|
|
743
|
+
return Number(rows[0]?.count ?? 0);
|
|
744
|
+
}
|
|
745
|
+
then(onfulfilled, onrejected) {
|
|
746
|
+
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
747
|
+
}
|
|
748
|
+
async attach(ids) {
|
|
749
|
+
const list = Array.isArray(ids) ? ids : [ids];
|
|
750
|
+
const parentId = this.parent.get(this.relation.parentKey);
|
|
751
|
+
const extraKeys = Object.keys(this.pivotValues);
|
|
752
|
+
const extraColumns = extraKeys.length > 0 ? `, ${extraKeys.join(", ")}` : "";
|
|
753
|
+
const extraPlaceholders = extraKeys.map((_, index) => `$${index + 3}`).join(", ");
|
|
754
|
+
const extraValues = extraKeys.map((key) => this.pivotValues[key]);
|
|
755
|
+
for (const id of list) {
|
|
756
|
+
await this.connection().unsafe(extraKeys.length > 0 ? `INSERT INTO ${this.relation.pivotTable} (${String(this.relation.foreignPivotKey)}, ${String(this.relation.relatedPivotKey)}${extraColumns}) VALUES ($1, $2, ${extraPlaceholders})` : `INSERT INTO ${this.relation.pivotTable} (${String(this.relation.foreignPivotKey)}, ${String(this.relation.relatedPivotKey)}) VALUES ($1, $2)`, [parentId, id, ...extraValues]);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
async toggle(ids) {
|
|
760
|
+
const list = Array.isArray(ids) ? ids : [ids];
|
|
761
|
+
const parentId = this.parent.get(this.relation.parentKey);
|
|
762
|
+
for (const id of list) {
|
|
763
|
+
const existing = await this.connection().unsafe(`SELECT 1 FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1 AND ${String(this.relation.relatedPivotKey)} = $2 LIMIT 1`, [parentId, id]);
|
|
764
|
+
if (existing.length > 0) {
|
|
765
|
+
await this.detach(id);
|
|
766
|
+
} else {
|
|
767
|
+
await this.attach(id);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
async detach(ids) {
|
|
772
|
+
const parentId = this.parent.get(this.relation.parentKey);
|
|
773
|
+
if (ids === undefined) {
|
|
774
|
+
await this.connection().unsafe(`DELETE FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1`, [parentId]);
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
const list = Array.isArray(ids) ? ids : [ids];
|
|
778
|
+
await this.connection().unsafe(`DELETE FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1 AND ${String(this.relation.relatedPivotKey)} = ANY($2)`, [parentId, list]);
|
|
779
|
+
}
|
|
780
|
+
async sync(ids) {
|
|
781
|
+
await this.detach();
|
|
782
|
+
if (ids.length > 0) {
|
|
783
|
+
await this.attach(ids);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
async create(attributes = {}) {
|
|
787
|
+
const related = await this.related.create(attributes);
|
|
788
|
+
await this.attach(related.id);
|
|
789
|
+
return related;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
class MorphManyRelationQuery {
|
|
794
|
+
parent;
|
|
795
|
+
related;
|
|
796
|
+
relation;
|
|
797
|
+
kind = "morphMany";
|
|
798
|
+
extraWhere = {};
|
|
799
|
+
extraOptions = {};
|
|
800
|
+
constructor(parent, related, relation) {
|
|
801
|
+
this.parent = parent;
|
|
802
|
+
this.related = related;
|
|
803
|
+
this.relation = relation;
|
|
804
|
+
}
|
|
805
|
+
where(where) {
|
|
806
|
+
this.extraWhere = { ...this.extraWhere, ...where };
|
|
807
|
+
return this;
|
|
808
|
+
}
|
|
809
|
+
applyEagerLoad(query, alias) {
|
|
810
|
+
query.withMorphMany(alias, this.relation, this.related.repository(), this.extraOptions);
|
|
811
|
+
}
|
|
812
|
+
hydrateEager(row, alias) {
|
|
813
|
+
const value = row[alias] ?? [];
|
|
814
|
+
const rows = Array.isArray(value) ? value : [];
|
|
815
|
+
return rows.map((item) => this.related.newFromRecord(item));
|
|
816
|
+
}
|
|
817
|
+
toExistsClause(parentTable) {
|
|
818
|
+
const childTable = this.related.repository().getTable().name;
|
|
819
|
+
const extra = buildAdvancedWhereClause(childTable, {
|
|
820
|
+
[this.relation.morphTypeKey]: this.relation.morphType,
|
|
821
|
+
...this.extraWhere
|
|
822
|
+
}, [], []);
|
|
823
|
+
const extraSql = extra.clause.replace(/^ WHERE /, "");
|
|
824
|
+
const sql = `SELECT 1 FROM ${quoteIdentifier(childTable)} WHERE ${qualifyColumn(childTable, this.relation.morphIdKey)} = ${qualifyColumn(parentTable, this.relation.localKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
|
|
825
|
+
return { sql, params: extra.params };
|
|
826
|
+
}
|
|
827
|
+
async get() {
|
|
828
|
+
const repository = this.related.repository().withConnection(this.parent.getRepository().getConnection());
|
|
829
|
+
const rows = await repository.query(asWhere({
|
|
830
|
+
[this.relation.morphTypeKey]: this.relation.morphType,
|
|
831
|
+
[this.relation.morphIdKey]: this.parent.get(this.relation.localKey),
|
|
832
|
+
...this.extraWhere
|
|
833
|
+
})).get();
|
|
834
|
+
return rows.map((row) => this.related.newFromRecord(row));
|
|
835
|
+
}
|
|
836
|
+
async first() {
|
|
837
|
+
const rows = await this.get();
|
|
838
|
+
return rows[0] ?? null;
|
|
839
|
+
}
|
|
840
|
+
async count() {
|
|
841
|
+
const repository = this.related.repository().withConnection(this.parent.getRepository().getConnection());
|
|
842
|
+
return repository.query(asWhere({
|
|
843
|
+
[this.relation.morphTypeKey]: this.relation.morphType,
|
|
844
|
+
[this.relation.morphIdKey]: this.parent.get(this.relation.localKey),
|
|
845
|
+
...this.extraWhere
|
|
846
|
+
})).count();
|
|
847
|
+
}
|
|
848
|
+
then(onfulfilled, onrejected) {
|
|
849
|
+
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
850
|
+
}
|
|
851
|
+
async create(attributes = {}) {
|
|
852
|
+
return this.related.create(attributes, {
|
|
853
|
+
[this.relation.morphTypeKey]: this.relation.morphType,
|
|
854
|
+
[this.relation.morphIdKey]: this.parent.get(this.relation.localKey)
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
class MorphOneRelationQuery {
|
|
860
|
+
relation;
|
|
861
|
+
kind = "morphOne";
|
|
862
|
+
inner;
|
|
863
|
+
constructor(parent, related, relation) {
|
|
864
|
+
this.relation = relation;
|
|
865
|
+
this.inner = new MorphManyRelationQuery(parent, related, {
|
|
866
|
+
type: "morphMany",
|
|
867
|
+
name: relation.name,
|
|
868
|
+
localKey: relation.localKey,
|
|
869
|
+
morphTypeKey: relation.morphTypeKey,
|
|
870
|
+
morphIdKey: relation.morphIdKey,
|
|
871
|
+
morphType: relation.morphType
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
where(where) {
|
|
875
|
+
this.inner.where(where);
|
|
876
|
+
return this;
|
|
877
|
+
}
|
|
878
|
+
applyEagerLoad(query, alias) {
|
|
879
|
+
this.inner.applyEagerLoad(query, alias);
|
|
880
|
+
}
|
|
881
|
+
hydrateEager(row, alias) {
|
|
882
|
+
const hydrated = this.inner.hydrateEager(row, alias);
|
|
883
|
+
return hydrated[0];
|
|
884
|
+
}
|
|
885
|
+
toExistsClause(parentTable) {
|
|
886
|
+
return this.inner.toExistsClause(parentTable);
|
|
887
|
+
}
|
|
888
|
+
async get() {
|
|
889
|
+
return this.inner.first();
|
|
890
|
+
}
|
|
891
|
+
async first() {
|
|
892
|
+
return this.get();
|
|
893
|
+
}
|
|
894
|
+
async count() {
|
|
895
|
+
return this.inner.count();
|
|
896
|
+
}
|
|
897
|
+
then(onfulfilled, onrejected) {
|
|
898
|
+
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
899
|
+
}
|
|
900
|
+
async create(attributes = {}) {
|
|
901
|
+
return this.inner.create(attributes);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
class MorphToRelationQuery {
|
|
906
|
+
parent;
|
|
907
|
+
relatedByType;
|
|
908
|
+
relation;
|
|
909
|
+
kind = "morphTo";
|
|
910
|
+
extraWhere = {};
|
|
911
|
+
constructor(parent, relatedByType, relation) {
|
|
912
|
+
this.parent = parent;
|
|
913
|
+
this.relatedByType = relatedByType;
|
|
914
|
+
this.relation = relation;
|
|
915
|
+
}
|
|
916
|
+
where(where) {
|
|
917
|
+
this.extraWhere = { ...this.extraWhere, ...where };
|
|
918
|
+
return this;
|
|
919
|
+
}
|
|
920
|
+
applyEagerLoad(query, alias) {
|
|
921
|
+
const repositories = new Map(Object.entries(this.relatedByType).map(([type, model]) => [
|
|
922
|
+
type,
|
|
923
|
+
model.repository()
|
|
924
|
+
]));
|
|
925
|
+
query.withMorphTo(alias, this.relation, repositories);
|
|
926
|
+
}
|
|
927
|
+
hydrateEager(row, alias) {
|
|
928
|
+
return row[alias];
|
|
929
|
+
}
|
|
930
|
+
toExistsClause(parentTable) {
|
|
931
|
+
const type = String(this.parent.get(this.relation.morphTypeKey) ?? "");
|
|
932
|
+
const related = this.relatedByType[type];
|
|
933
|
+
if (!related) {
|
|
934
|
+
return { sql: "SELECT 1 WHERE 1 = 0", params: [] };
|
|
935
|
+
}
|
|
936
|
+
const relatedTable = related.repository().getTable();
|
|
937
|
+
const extra = buildAdvancedWhereClause(relatedTable.name, this.extraWhere, [], []);
|
|
938
|
+
const extraSql = extra.clause.replace(/^ WHERE /, "");
|
|
939
|
+
return {
|
|
940
|
+
sql: `SELECT 1 FROM ${quoteIdentifier(relatedTable.name)} WHERE ${qualifyColumn(relatedTable.name, relatedTable.primaryKey)} = ${qualifyColumn(parentTable, this.relation.morphIdKey)}${extraSql ? ` AND ${extraSql}` : ""}`,
|
|
941
|
+
params: extra.params
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
async get() {
|
|
945
|
+
const type = String(this.parent.get(this.relation.morphTypeKey) ?? "");
|
|
946
|
+
const id = this.parent.get(this.relation.morphIdKey);
|
|
947
|
+
const related = this.relatedByType[type];
|
|
948
|
+
if (!related || id === null || id === undefined) {
|
|
949
|
+
return null;
|
|
950
|
+
}
|
|
951
|
+
const table = related.repository().getTable();
|
|
952
|
+
const row = await related.repository().withConnection(this.parent.getRepository().getConnection()).query(asWhere({ [table.primaryKey]: id, ...this.extraWhere })).first();
|
|
953
|
+
return row ? related.newFromRecord(row) : null;
|
|
954
|
+
}
|
|
955
|
+
then(onfulfilled, onrejected) {
|
|
956
|
+
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
419
960
|
// ../../src/core/database/relationships.ts
|
|
420
961
|
function hasMany(definition) {
|
|
421
962
|
return {
|
|
@@ -564,8 +1105,77 @@ function indexMorphToRelation(children, parentsByType, relation) {
|
|
|
564
1105
|
|
|
565
1106
|
// ../../src/core/database/model.ts
|
|
566
1107
|
var modelRepositories = new WeakMap;
|
|
1108
|
+
var namedModels = new Map;
|
|
567
1109
|
var modelGlobalScopes = new WeakMap;
|
|
1110
|
+
var modelObservers = new WeakMap;
|
|
568
1111
|
var modelBooted = new WeakSet;
|
|
1112
|
+
async function runObservers(model, hook) {
|
|
1113
|
+
const observers = modelObservers.get(model.constructor) ?? [];
|
|
1114
|
+
for (const observer of observers) {
|
|
1115
|
+
const handler = observer[hook];
|
|
1116
|
+
if (handler && await handler(model) === false) {
|
|
1117
|
+
return false;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
return true;
|
|
1121
|
+
}
|
|
1122
|
+
function accessorName(key) {
|
|
1123
|
+
const pascal = key.replace(/_([a-z])/g, (_match, letter) => letter.toUpperCase());
|
|
1124
|
+
return `get${pascal.charAt(0).toUpperCase()}${pascal.slice(1)}Attribute`;
|
|
1125
|
+
}
|
|
1126
|
+
function isLoadableModel(value) {
|
|
1127
|
+
return Boolean(value && typeof value === "object" && typeof value.load === "function" && typeof value.loaded === "function" && typeof value.setLoaded === "function");
|
|
1128
|
+
}
|
|
1129
|
+
async function eagerLoadOnModels(models, paths) {
|
|
1130
|
+
if (models.length === 0 || paths.length === 0) {
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
const grouped = new Map;
|
|
1134
|
+
for (const path of paths) {
|
|
1135
|
+
const [head, ...rest] = path.split(".");
|
|
1136
|
+
if (!head) {
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
const nested = rest.join(".");
|
|
1140
|
+
const existing = grouped.get(head) ?? [];
|
|
1141
|
+
if (nested) {
|
|
1142
|
+
existing.push(nested);
|
|
1143
|
+
}
|
|
1144
|
+
grouped.set(head, existing);
|
|
1145
|
+
}
|
|
1146
|
+
for (const [head, nested] of grouped) {
|
|
1147
|
+
const unloaded = models.filter((model) => model.loaded(head) === undefined);
|
|
1148
|
+
if (unloaded.length > 0) {
|
|
1149
|
+
const first = unloaded[0];
|
|
1150
|
+
if (!first) {
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
const method = first[head];
|
|
1154
|
+
if (typeof method !== "function") {
|
|
1155
|
+
throw new Error(`${first.constructor.name} has no relation method ${head}().`);
|
|
1156
|
+
}
|
|
1157
|
+
const relationQuery = method.call(first);
|
|
1158
|
+
const query = first.getRepository().query();
|
|
1159
|
+
relationQuery.applyEagerLoad(query, head);
|
|
1160
|
+
const attached = await query.attachToRows(unloaded.map((model) => model.toObject()));
|
|
1161
|
+
for (const [index, model] of unloaded.entries()) {
|
|
1162
|
+
const row = attached[index] ?? model.toObject();
|
|
1163
|
+
model.setLoaded(head, relationQuery.hydrateEager(row, head));
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
if (nested.length === 0) {
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
const children = models.flatMap((model) => {
|
|
1170
|
+
const loaded = model.loaded(head);
|
|
1171
|
+
return Array.isArray(loaded) ? loaded : loaded ? [loaded] : [];
|
|
1172
|
+
});
|
|
1173
|
+
await eagerLoadOnModels(children.filter(isLoadableModel), nested);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
async function loadNested(model, path) {
|
|
1177
|
+
await eagerLoadOnModels([model], [path]);
|
|
1178
|
+
}
|
|
569
1179
|
function resolveModelRepository(model) {
|
|
570
1180
|
const repository = modelRepositories.get(model);
|
|
571
1181
|
if (!repository) {
|
|
@@ -573,6 +1183,48 @@ function resolveModelRepository(model) {
|
|
|
573
1183
|
}
|
|
574
1184
|
return repository;
|
|
575
1185
|
}
|
|
1186
|
+
function registerModelClass(name, model) {
|
|
1187
|
+
namedModels.set(name, model);
|
|
1188
|
+
}
|
|
1189
|
+
function resolveRelated(related) {
|
|
1190
|
+
if (typeof related === "string") {
|
|
1191
|
+
const found = namedModels.get(related);
|
|
1192
|
+
if (!found) {
|
|
1193
|
+
throw new Error(`Model [${related}] is not registered. Call registerModelClass() first.`);
|
|
1194
|
+
}
|
|
1195
|
+
return found;
|
|
1196
|
+
}
|
|
1197
|
+
if (typeof related === "function" && typeof related.repository !== "function") {
|
|
1198
|
+
return related();
|
|
1199
|
+
}
|
|
1200
|
+
return related;
|
|
1201
|
+
}
|
|
1202
|
+
function inferRelationMethodName(callee) {
|
|
1203
|
+
const stack = new Error().stack ?? "";
|
|
1204
|
+
let seenCallee = false;
|
|
1205
|
+
for (const line of stack.split(`
|
|
1206
|
+
`)) {
|
|
1207
|
+
const match = /at (?:async )?(?:[^.\s]+\.)?(\w+)/.exec(line);
|
|
1208
|
+
const name = match?.[1];
|
|
1209
|
+
if (!name || name === "Error" || name === "inferRelationMethodName") {
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
if (!seenCallee) {
|
|
1213
|
+
if (name === callee) {
|
|
1214
|
+
seenCallee = true;
|
|
1215
|
+
}
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
if (name !== callee) {
|
|
1219
|
+
return name;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
function morphClassOf(model) {
|
|
1225
|
+
const statics = modelStatics(model.constructor === Function ? model : model.constructor);
|
|
1226
|
+
return statics.$morphClass ?? (model.constructor === Function ? model.name : model.constructor.name);
|
|
1227
|
+
}
|
|
576
1228
|
function modelStatics(model) {
|
|
577
1229
|
return model;
|
|
578
1230
|
}
|
|
@@ -602,6 +1254,11 @@ function hydrateValue(value, cast) {
|
|
|
602
1254
|
case "bool":
|
|
603
1255
|
case "boolean":
|
|
604
1256
|
return value === true || value === 1 || value === "1" || value === "true";
|
|
1257
|
+
case "integer":
|
|
1258
|
+
case "int":
|
|
1259
|
+
return value === "" ? null : Number(value);
|
|
1260
|
+
case "hashed":
|
|
1261
|
+
return value;
|
|
605
1262
|
default:
|
|
606
1263
|
return value;
|
|
607
1264
|
}
|
|
@@ -619,6 +1276,11 @@ function dehydrateValue(value, cast) {
|
|
|
619
1276
|
case "bool":
|
|
620
1277
|
case "boolean":
|
|
621
1278
|
return Boolean(value);
|
|
1279
|
+
case "integer":
|
|
1280
|
+
case "int":
|
|
1281
|
+
return value === "" ? null : Number(value);
|
|
1282
|
+
case "hashed":
|
|
1283
|
+
return value;
|
|
622
1284
|
default:
|
|
623
1285
|
return value;
|
|
624
1286
|
}
|
|
@@ -673,6 +1335,153 @@ function applyTimestampsOnUpdate(columns, values, enabled) {
|
|
|
673
1335
|
return result;
|
|
674
1336
|
}
|
|
675
1337
|
|
|
1338
|
+
class ModelQuery {
|
|
1339
|
+
modelClass;
|
|
1340
|
+
query;
|
|
1341
|
+
eager = [];
|
|
1342
|
+
constructor(modelClass, query) {
|
|
1343
|
+
this.modelClass = modelClass;
|
|
1344
|
+
this.query = query;
|
|
1345
|
+
}
|
|
1346
|
+
with(...relations) {
|
|
1347
|
+
const statics = modelStatics(this.modelClass);
|
|
1348
|
+
ensureBooted(this.modelClass);
|
|
1349
|
+
const dummy = statics.newFromRecord({}, false);
|
|
1350
|
+
for (const path of relations) {
|
|
1351
|
+
const name = path.split(".")[0] ?? path;
|
|
1352
|
+
const method = dummy[name];
|
|
1353
|
+
if (typeof method !== "function") {
|
|
1354
|
+
throw new Error(`${this.modelClass.name} has no relation method ${name}().`);
|
|
1355
|
+
}
|
|
1356
|
+
const relationQuery = method.call(dummy);
|
|
1357
|
+
this.eager.push({ name, path, relationQuery });
|
|
1358
|
+
relationQuery.applyEagerLoad(this.query, name);
|
|
1359
|
+
}
|
|
1360
|
+
return this;
|
|
1361
|
+
}
|
|
1362
|
+
where(input) {
|
|
1363
|
+
this.query.where(input);
|
|
1364
|
+
return this;
|
|
1365
|
+
}
|
|
1366
|
+
orWhere(input) {
|
|
1367
|
+
this.query.orWhere(input);
|
|
1368
|
+
return this;
|
|
1369
|
+
}
|
|
1370
|
+
orderBy(orderBy) {
|
|
1371
|
+
this.query.orderBy(orderBy);
|
|
1372
|
+
return this;
|
|
1373
|
+
}
|
|
1374
|
+
limit(limit) {
|
|
1375
|
+
this.query.limit(limit);
|
|
1376
|
+
return this;
|
|
1377
|
+
}
|
|
1378
|
+
offset(offset) {
|
|
1379
|
+
this.query.offset(offset);
|
|
1380
|
+
return this;
|
|
1381
|
+
}
|
|
1382
|
+
whereNull(column) {
|
|
1383
|
+
this.query.whereNull(column);
|
|
1384
|
+
return this;
|
|
1385
|
+
}
|
|
1386
|
+
whereIn(column, values) {
|
|
1387
|
+
this.query.whereIn(column, values);
|
|
1388
|
+
return this;
|
|
1389
|
+
}
|
|
1390
|
+
whereExists(sql, params = []) {
|
|
1391
|
+
this.query.whereExists(sql, params);
|
|
1392
|
+
return this;
|
|
1393
|
+
}
|
|
1394
|
+
whereNotExists(sql, params = []) {
|
|
1395
|
+
this.query.whereNotExists(sql, params);
|
|
1396
|
+
return this;
|
|
1397
|
+
}
|
|
1398
|
+
whereHas(name, constrain) {
|
|
1399
|
+
return this.constrainExists(name, constrain, false);
|
|
1400
|
+
}
|
|
1401
|
+
has(name) {
|
|
1402
|
+
return this.constrainExists(name, undefined, false);
|
|
1403
|
+
}
|
|
1404
|
+
doesntHave(name) {
|
|
1405
|
+
return this.constrainExists(name, undefined, true);
|
|
1406
|
+
}
|
|
1407
|
+
whereDoesntHave(name, constrain) {
|
|
1408
|
+
return this.constrainExists(name, constrain, true);
|
|
1409
|
+
}
|
|
1410
|
+
withHasMany(...args) {
|
|
1411
|
+
this.query.withHasMany(...args);
|
|
1412
|
+
return this;
|
|
1413
|
+
}
|
|
1414
|
+
withBelongsTo(...args) {
|
|
1415
|
+
this.query.withBelongsTo(...args);
|
|
1416
|
+
return this;
|
|
1417
|
+
}
|
|
1418
|
+
withBelongsToMany(...args) {
|
|
1419
|
+
this.query.withBelongsToMany(...args);
|
|
1420
|
+
return this;
|
|
1421
|
+
}
|
|
1422
|
+
withMorphMany(...args) {
|
|
1423
|
+
this.query.withMorphMany(...args);
|
|
1424
|
+
return this;
|
|
1425
|
+
}
|
|
1426
|
+
withMorphOne(...args) {
|
|
1427
|
+
this.query.withMorphOne(...args);
|
|
1428
|
+
return this;
|
|
1429
|
+
}
|
|
1430
|
+
withMorphTo(...args) {
|
|
1431
|
+
this.query.withMorphTo(...args);
|
|
1432
|
+
return this;
|
|
1433
|
+
}
|
|
1434
|
+
async get() {
|
|
1435
|
+
const statics = modelStatics(this.modelClass);
|
|
1436
|
+
const rows = await this.query.get();
|
|
1437
|
+
const models = [];
|
|
1438
|
+
for (const row of rows) {
|
|
1439
|
+
const model = statics.newFromRecord(row, true);
|
|
1440
|
+
await runObservers(model, "retrieved");
|
|
1441
|
+
for (const { name, relationQuery } of this.eager) {
|
|
1442
|
+
model.setLoaded(name, relationQuery.hydrateEager(row, name));
|
|
1443
|
+
}
|
|
1444
|
+
models.push(model);
|
|
1445
|
+
}
|
|
1446
|
+
const nested = this.eager.filter((item) => item.path.includes(".")).map((item) => item.path);
|
|
1447
|
+
await eagerLoadOnModels(models.filter(isLoadableModel), nested);
|
|
1448
|
+
return models;
|
|
1449
|
+
}
|
|
1450
|
+
async first() {
|
|
1451
|
+
this.query.limit(1);
|
|
1452
|
+
const models = await this.get();
|
|
1453
|
+
return models[0] ?? null;
|
|
1454
|
+
}
|
|
1455
|
+
async find(id) {
|
|
1456
|
+
const primaryKey = resolveModelRepository(this.modelClass).getTable().primaryKey;
|
|
1457
|
+
return this.where({ [primaryKey]: id }).first();
|
|
1458
|
+
}
|
|
1459
|
+
async findOrFail(id, errorFactory) {
|
|
1460
|
+
const model = await this.find(id);
|
|
1461
|
+
if (model) {
|
|
1462
|
+
return model;
|
|
1463
|
+
}
|
|
1464
|
+
throw errorFactory?.(id) ?? new NotFoundError(`${this.modelClass.name} ${String(id)} not found.`);
|
|
1465
|
+
}
|
|
1466
|
+
then(onfulfilled, onrejected) {
|
|
1467
|
+
return this.get().then(onfulfilled ?? undefined, onrejected ?? undefined);
|
|
1468
|
+
}
|
|
1469
|
+
constrainExists(name, constrain, not) {
|
|
1470
|
+
const statics = modelStatics(this.modelClass);
|
|
1471
|
+
ensureBooted(this.modelClass);
|
|
1472
|
+
const repository = resolveModelRepository(this.modelClass);
|
|
1473
|
+
const dummy = statics.newFromRecord({});
|
|
1474
|
+
const method = dummy[name];
|
|
1475
|
+
if (typeof method !== "function") {
|
|
1476
|
+
throw new Error(`${this.modelClass.name} has no relation method ${name}().`);
|
|
1477
|
+
}
|
|
1478
|
+
const relationQuery = method.call(dummy);
|
|
1479
|
+
constrain?.(relationQuery);
|
|
1480
|
+
const exists = relationQuery.toExistsClause(repository.getTable().name);
|
|
1481
|
+
return not ? this.whereNotExists(exists.sql, exists.params) : this.whereExists(exists.sql, exists.params);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
676
1485
|
class Model {
|
|
677
1486
|
attributes;
|
|
678
1487
|
repository;
|
|
@@ -680,11 +1489,23 @@ class Model {
|
|
|
680
1489
|
static $guarded;
|
|
681
1490
|
static $casts = {};
|
|
682
1491
|
static $timestamps = true;
|
|
1492
|
+
static $hidden;
|
|
1493
|
+
static $visible;
|
|
1494
|
+
static $appends;
|
|
1495
|
+
static $morphClass;
|
|
683
1496
|
_exists;
|
|
1497
|
+
loadedRelations = {};
|
|
1498
|
+
hiddenOverrides = [];
|
|
1499
|
+
visibleOverrides = [];
|
|
1500
|
+
appended = [];
|
|
684
1501
|
constructor(attributes, repository, exists = true) {
|
|
685
1502
|
this.attributes = attributes;
|
|
686
1503
|
this.repository = repository;
|
|
687
1504
|
this._exists = exists;
|
|
1505
|
+
this.attributes = modelStatics(this.constructor).hydrateAttributes(attributes);
|
|
1506
|
+
}
|
|
1507
|
+
getRepository() {
|
|
1508
|
+
return this.repository;
|
|
688
1509
|
}
|
|
689
1510
|
get $exists() {
|
|
690
1511
|
return this._exists;
|
|
@@ -698,8 +1519,51 @@ class Model {
|
|
|
698
1519
|
toObject() {
|
|
699
1520
|
return { ...this.attributes };
|
|
700
1521
|
}
|
|
1522
|
+
toArray() {
|
|
1523
|
+
const ModelClass = modelStatics(this.constructor);
|
|
1524
|
+
const hidden = new Set([...ModelClass.$hidden ?? [], ...this.hiddenOverrides]);
|
|
1525
|
+
const visible = this.visibleOverrides.length > 0 ? this.visibleOverrides : ModelClass.$visible;
|
|
1526
|
+
const data = { ...this.attributes };
|
|
1527
|
+
if (visible && visible.length > 0) {
|
|
1528
|
+
for (const key of Object.keys(data)) {
|
|
1529
|
+
if (!visible.includes(key)) {
|
|
1530
|
+
delete data[key];
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
for (const key of hidden) {
|
|
1535
|
+
delete data[key];
|
|
1536
|
+
}
|
|
1537
|
+
for (const key of [...ModelClass.$appends ?? [], ...this.appended]) {
|
|
1538
|
+
const accessor = this[accessorName(key)];
|
|
1539
|
+
if (typeof accessor === "function") {
|
|
1540
|
+
data[key] = accessor.call(this);
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
for (const [name, value] of Object.entries(this.loadedRelations)) {
|
|
1544
|
+
if (!hidden.has(name) && (!visible || visible.includes(name) || this.appended.includes(name))) {
|
|
1545
|
+
data[name] = value;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
return data;
|
|
1549
|
+
}
|
|
1550
|
+
toJSON() {
|
|
1551
|
+
return this.toArray();
|
|
1552
|
+
}
|
|
1553
|
+
makeHidden(...keys) {
|
|
1554
|
+
this.hiddenOverrides.push(...keys);
|
|
1555
|
+
return this;
|
|
1556
|
+
}
|
|
1557
|
+
makeVisible(...keys) {
|
|
1558
|
+
this.visibleOverrides.push(...keys);
|
|
1559
|
+
return this;
|
|
1560
|
+
}
|
|
1561
|
+
append(...keys) {
|
|
1562
|
+
this.appended.push(...keys);
|
|
1563
|
+
return this;
|
|
1564
|
+
}
|
|
701
1565
|
primaryKey() {
|
|
702
|
-
|
|
1566
|
+
return this.repository.getTable().primaryKey;
|
|
703
1567
|
}
|
|
704
1568
|
static primaryKeyField() {
|
|
705
1569
|
return resolveModelRepository(this).getTable().primaryKey;
|
|
@@ -718,6 +1582,10 @@ class Model {
|
|
|
718
1582
|
return new statics(hydrated, repository, exists);
|
|
719
1583
|
}
|
|
720
1584
|
static boot() {}
|
|
1585
|
+
static observe(observer) {
|
|
1586
|
+
const existing = modelObservers.get(this) ?? [];
|
|
1587
|
+
modelObservers.set(this, [...existing, observer]);
|
|
1588
|
+
}
|
|
721
1589
|
static addGlobalScope(_name, scope) {
|
|
722
1590
|
ensureBooted(this);
|
|
723
1591
|
const existing = modelGlobalScopes.get(this) ?? [];
|
|
@@ -736,26 +1604,55 @@ class Model {
|
|
|
736
1604
|
for (const scope of getGlobalScopes(this)) {
|
|
737
1605
|
query = scope(query);
|
|
738
1606
|
}
|
|
739
|
-
return query;
|
|
1607
|
+
return new ModelQuery(this, query);
|
|
1608
|
+
}
|
|
1609
|
+
static newFromRecord(record, exists = true) {
|
|
1610
|
+
const repository = resolveModelRepository(this);
|
|
1611
|
+
return modelStatics(this).fromRecord(record, repository, exists);
|
|
740
1612
|
}
|
|
741
|
-
static async create(attributes) {
|
|
1613
|
+
static async create(attributes, forced = {}) {
|
|
742
1614
|
const statics = modelStatics(this);
|
|
743
1615
|
ensureBooted(this);
|
|
744
1616
|
const repository = resolveModelRepository(this);
|
|
745
1617
|
const table = repository.getTable();
|
|
746
1618
|
const timestamps = statics.$timestamps ?? true;
|
|
747
|
-
const assignable =
|
|
1619
|
+
const assignable = {
|
|
1620
|
+
...filterMassAssignable(statics.$fillable, statics.$guarded, attributes),
|
|
1621
|
+
...forced
|
|
1622
|
+
};
|
|
748
1623
|
const withTimestamps = applyTimestampsOnCreate(table.columns, assignable, timestamps);
|
|
749
1624
|
const payload = statics.dehydrateAttributes(withTimestamps);
|
|
1625
|
+
const pending = statics.newFromRecord({ ...payload }, false);
|
|
1626
|
+
if (await runObservers(pending, "saving") === false) {
|
|
1627
|
+
throw new Error(`${this.name}.create() was cancelled by an observer.`);
|
|
1628
|
+
}
|
|
1629
|
+
if (await runObservers(pending, "creating") === false) {
|
|
1630
|
+
throw new Error(`${this.name}.create() was cancelled by an observer.`);
|
|
1631
|
+
}
|
|
750
1632
|
const record = await repository.create(payload);
|
|
751
|
-
|
|
1633
|
+
const created = statics.fromRecord(record, repository, true);
|
|
1634
|
+
await runObservers(created, "created");
|
|
1635
|
+
await runObservers(created, "saved");
|
|
1636
|
+
return created;
|
|
1637
|
+
}
|
|
1638
|
+
static with(...relations) {
|
|
1639
|
+
return Model.query.call(this).with(...relations);
|
|
1640
|
+
}
|
|
1641
|
+
static whereHas(name, constrain) {
|
|
1642
|
+
return Model.query.call(this).whereHas(name, constrain);
|
|
1643
|
+
}
|
|
1644
|
+
static has(name) {
|
|
1645
|
+
return Model.query.call(this).has(name);
|
|
1646
|
+
}
|
|
1647
|
+
static doesntHave(name) {
|
|
1648
|
+
return Model.query.call(this).doesntHave(name);
|
|
1649
|
+
}
|
|
1650
|
+
static whereDoesntHave(name, constrain) {
|
|
1651
|
+
return Model.query.call(this).whereDoesntHave(name, constrain);
|
|
752
1652
|
}
|
|
753
1653
|
static async find(id) {
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
const primaryKey = repository.getTable().primaryKey;
|
|
757
|
-
const record = await Model.query.call(this).where({ [primaryKey]: id }).first();
|
|
758
|
-
return record ? statics.fromRecord(record, repository, true) : null;
|
|
1654
|
+
const primaryKey = resolveModelRepository(this).getTable().primaryKey;
|
|
1655
|
+
return Model.query.call(this).where({ [primaryKey]: id }).first();
|
|
759
1656
|
}
|
|
760
1657
|
static async findOrFail(id, errorFactory) {
|
|
761
1658
|
const model = await Model.find.call(this, id);
|
|
@@ -765,8 +1662,6 @@ class Model {
|
|
|
765
1662
|
throw errorFactory?.(id) ?? new NotFoundError(`${this.name} ${String(id)} not found.`);
|
|
766
1663
|
}
|
|
767
1664
|
static async all(options = {}) {
|
|
768
|
-
const statics = modelStatics(this);
|
|
769
|
-
const repository = resolveModelRepository(this);
|
|
770
1665
|
let query = Model.query.call(this);
|
|
771
1666
|
if (options.orderBy) {
|
|
772
1667
|
query = query.orderBy(options.orderBy);
|
|
@@ -774,28 +1669,57 @@ class Model {
|
|
|
774
1669
|
if (options.limit !== undefined) {
|
|
775
1670
|
query = query.limit(options.limit);
|
|
776
1671
|
}
|
|
777
|
-
|
|
778
|
-
|
|
1672
|
+
return query.get();
|
|
1673
|
+
}
|
|
1674
|
+
static where(where) {
|
|
1675
|
+
return Model.query.call(this).where(where);
|
|
779
1676
|
}
|
|
780
1677
|
static async firstWhere(where, options = {}) {
|
|
781
|
-
const statics = modelStatics(this);
|
|
782
|
-
const repository = resolveModelRepository(this);
|
|
783
1678
|
let query = Model.query.call(this).where(where);
|
|
784
1679
|
if (options.orderBy) {
|
|
785
1680
|
query = query.orderBy(options.orderBy);
|
|
786
1681
|
}
|
|
787
|
-
|
|
788
|
-
|
|
1682
|
+
return query.first();
|
|
1683
|
+
}
|
|
1684
|
+
static async firstOrNew(where, values = {}) {
|
|
1685
|
+
const existing = await Model.firstWhere.call(this, where);
|
|
1686
|
+
if (existing) {
|
|
1687
|
+
return existing;
|
|
1688
|
+
}
|
|
1689
|
+
return modelStatics(this).newFromRecord({ ...where, ...values }, false);
|
|
1690
|
+
}
|
|
1691
|
+
static async firstOrCreate(where, values = {}) {
|
|
1692
|
+
const existing = await Model.firstWhere.call(this, where);
|
|
1693
|
+
if (existing) {
|
|
1694
|
+
return existing;
|
|
1695
|
+
}
|
|
1696
|
+
return Model.create.call(this, { ...where, ...values });
|
|
1697
|
+
}
|
|
1698
|
+
static async updateOrCreate(where, values = {}) {
|
|
1699
|
+
const existing = await Model.firstWhere.call(this, where);
|
|
1700
|
+
if (existing) {
|
|
1701
|
+
return existing.update(values);
|
|
1702
|
+
}
|
|
1703
|
+
return Model.create.call(this, { ...where, ...values });
|
|
789
1704
|
}
|
|
790
1705
|
async save() {
|
|
791
1706
|
const ModelClass = modelStatics(this.constructor);
|
|
792
1707
|
const timestamps = ModelClass.$timestamps ?? true;
|
|
793
1708
|
const casts = ModelClass.$casts ?? {};
|
|
794
1709
|
const table = this.repository.getTable();
|
|
795
|
-
|
|
1710
|
+
const updating = this.$exists;
|
|
1711
|
+
if (await runObservers(this, "saving") === false) {
|
|
1712
|
+
return this;
|
|
1713
|
+
}
|
|
1714
|
+
if (await runObservers(this, updating ? "updating" : "creating") === false) {
|
|
1715
|
+
return this;
|
|
1716
|
+
}
|
|
1717
|
+
if (updating) {
|
|
796
1718
|
const changes = applyTimestampsOnUpdate(table.columns, applyCasts(this.attributes, casts, "dehydrate"), timestamps);
|
|
797
1719
|
const record2 = await this.repository.updateByIdOrThrow(this.id, changes);
|
|
798
1720
|
this.attributes = ModelClass.hydrateAttributes(record2);
|
|
1721
|
+
await runObservers(this, "updated");
|
|
1722
|
+
await runObservers(this, "saved");
|
|
799
1723
|
return this;
|
|
800
1724
|
}
|
|
801
1725
|
const assignable = filterMassAssignable(ModelClass.$fillable, ModelClass.$guarded, this.attributes);
|
|
@@ -804,6 +1728,8 @@ class Model {
|
|
|
804
1728
|
const record = await this.repository.create(payload);
|
|
805
1729
|
this.attributes = ModelClass.hydrateAttributes(record);
|
|
806
1730
|
this._exists = true;
|
|
1731
|
+
await runObservers(this, "created");
|
|
1732
|
+
await runObservers(this, "saved");
|
|
807
1733
|
return this;
|
|
808
1734
|
}
|
|
809
1735
|
async update(changes) {
|
|
@@ -813,10 +1739,14 @@ class Model {
|
|
|
813
1739
|
return await this.save();
|
|
814
1740
|
}
|
|
815
1741
|
async delete() {
|
|
816
|
-
if (
|
|
817
|
-
return
|
|
1742
|
+
if (await runObservers(this, "deleting") === false) {
|
|
1743
|
+
return false;
|
|
818
1744
|
}
|
|
819
|
-
|
|
1745
|
+
const deleted = resolveSoftDeleteColumn(this.repository.getTable()) ? await this.repository.deleteById(this.id) : await this.repository.forceDeleteById(this.id);
|
|
1746
|
+
if (deleted) {
|
|
1747
|
+
await runObservers(this, "deleted");
|
|
1748
|
+
}
|
|
1749
|
+
return deleted;
|
|
820
1750
|
}
|
|
821
1751
|
async forceDelete() {
|
|
822
1752
|
return await this.repository.forceDeleteById(this.id);
|
|
@@ -865,6 +1795,102 @@ class Model {
|
|
|
865
1795
|
const loaded = grouped.get(parentId) ?? [];
|
|
866
1796
|
return Object.assign(this, { [as]: loaded });
|
|
867
1797
|
}
|
|
1798
|
+
hasMany(related, foreignKey, localKey) {
|
|
1799
|
+
const table = this.repository.getTable();
|
|
1800
|
+
const relatedClass = resolveRelated(related);
|
|
1801
|
+
return new HasManyRelationQuery(this, relatedClass, hasMany({
|
|
1802
|
+
name: relatedClass.repository().getTable().name,
|
|
1803
|
+
localKey: localKey ?? table.primaryKey,
|
|
1804
|
+
foreignKey: foreignKey ?? foreignKeyFromTable(table.name)
|
|
1805
|
+
}));
|
|
1806
|
+
}
|
|
1807
|
+
hasOne(related, foreignKey, localKey) {
|
|
1808
|
+
const table = this.repository.getTable();
|
|
1809
|
+
const relatedClass = resolveRelated(related);
|
|
1810
|
+
return new HasOneRelationQuery(this, relatedClass, hasOne({
|
|
1811
|
+
name: relatedClass.repository().getTable().name,
|
|
1812
|
+
localKey: localKey ?? table.primaryKey,
|
|
1813
|
+
foreignKey: foreignKey ?? foreignKeyFromTable(table.name)
|
|
1814
|
+
}));
|
|
1815
|
+
}
|
|
1816
|
+
belongsTo(related, foreignKey, ownerKey) {
|
|
1817
|
+
const relatedClass = resolveRelated(related);
|
|
1818
|
+
const relatedTable = relatedClass.repository().getTable();
|
|
1819
|
+
return new BelongsToRelationQuery(this, relatedClass, belongsTo({
|
|
1820
|
+
name: relatedTable.name,
|
|
1821
|
+
foreignKey: foreignKey ?? foreignKeyFromTable(relatedTable.name),
|
|
1822
|
+
ownerKey: ownerKey ?? relatedTable.primaryKey
|
|
1823
|
+
}));
|
|
1824
|
+
}
|
|
1825
|
+
belongsToMany(related, pivotTable, foreignPivotKey, relatedPivotKey) {
|
|
1826
|
+
const table = this.repository.getTable();
|
|
1827
|
+
const relatedClass = resolveRelated(related);
|
|
1828
|
+
const relatedTable = relatedClass.repository().getTable();
|
|
1829
|
+
return new BelongsToManyRelationQuery(this, relatedClass, belongsToMany({
|
|
1830
|
+
name: relatedTable.name,
|
|
1831
|
+
pivotTable: pivotTable ?? pivotTableName(table.name, relatedTable.name),
|
|
1832
|
+
parentKey: table.primaryKey,
|
|
1833
|
+
relatedKey: relatedTable.primaryKey,
|
|
1834
|
+
foreignPivotKey: foreignPivotKey ?? foreignKeyFromTable(table.name),
|
|
1835
|
+
relatedPivotKey: relatedPivotKey ?? foreignKeyFromTable(relatedTable.name)
|
|
1836
|
+
}));
|
|
1837
|
+
}
|
|
1838
|
+
morphMany(related, morphName, typeKey, idKey, morphType) {
|
|
1839
|
+
const table = this.repository.getTable();
|
|
1840
|
+
const relatedClass = resolveRelated(related);
|
|
1841
|
+
return new MorphManyRelationQuery(this, relatedClass, morphMany({
|
|
1842
|
+
name: morphName,
|
|
1843
|
+
localKey: table.primaryKey,
|
|
1844
|
+
morphTypeKey: typeKey ?? `${morphName}_type`,
|
|
1845
|
+
morphIdKey: idKey ?? `${morphName}_id`,
|
|
1846
|
+
morphType: morphType ?? morphClassOf(this)
|
|
1847
|
+
}));
|
|
1848
|
+
}
|
|
1849
|
+
morphOne(related, morphName, typeKey, idKey, morphType) {
|
|
1850
|
+
const table = this.repository.getTable();
|
|
1851
|
+
const relatedClass = resolveRelated(related);
|
|
1852
|
+
return new MorphOneRelationQuery(this, relatedClass, morphOne({
|
|
1853
|
+
name: morphName,
|
|
1854
|
+
localKey: table.primaryKey,
|
|
1855
|
+
morphTypeKey: typeKey ?? `${morphName}_type`,
|
|
1856
|
+
morphIdKey: idKey ?? `${morphName}_id`,
|
|
1857
|
+
morphType: morphType ?? morphClassOf(this)
|
|
1858
|
+
}));
|
|
1859
|
+
}
|
|
1860
|
+
morphTo(relatedByType, morphName, typeKey, idKey) {
|
|
1861
|
+
const resolvedName = morphName ?? inferRelationMethodName("morphTo");
|
|
1862
|
+
if (!resolvedName) {
|
|
1863
|
+
throw new Error(`${this.constructor.name}.morphTo() needs an explicit morph name (Laravel infers it from the relation method).`);
|
|
1864
|
+
}
|
|
1865
|
+
const resolvedMap = Object.fromEntries(Object.entries(relatedByType).map(([type, related]) => [type, resolveRelated(related)]));
|
|
1866
|
+
return new MorphToRelationQuery(this, resolvedMap, morphTo({
|
|
1867
|
+
name: resolvedName,
|
|
1868
|
+
morphTypeKey: typeKey ?? `${resolvedName}_type`,
|
|
1869
|
+
morphIdKey: idKey ?? `${resolvedName}_id`
|
|
1870
|
+
}));
|
|
1871
|
+
}
|
|
1872
|
+
async load(...names) {
|
|
1873
|
+
for (const name of names) {
|
|
1874
|
+
if (name.includes(".")) {
|
|
1875
|
+
await loadNested(this, name);
|
|
1876
|
+
continue;
|
|
1877
|
+
}
|
|
1878
|
+
const method = this[name];
|
|
1879
|
+
if (typeof method !== "function") {
|
|
1880
|
+
throw new Error(`${this.constructor.name} has no relation method ${name}().`);
|
|
1881
|
+
}
|
|
1882
|
+
const relationQuery = method.call(this);
|
|
1883
|
+
this.loadedRelations[name] = await relationQuery.get();
|
|
1884
|
+
}
|
|
1885
|
+
return this;
|
|
1886
|
+
}
|
|
1887
|
+
loaded(name) {
|
|
1888
|
+
return this.loadedRelations[name];
|
|
1889
|
+
}
|
|
1890
|
+
setLoaded(name, value) {
|
|
1891
|
+
this.loadedRelations[name] = value;
|
|
1892
|
+
return this;
|
|
1893
|
+
}
|
|
868
1894
|
mergeAttributes(patch) {
|
|
869
1895
|
Object.assign(this.attributes, patch);
|
|
870
1896
|
return this;
|
|
@@ -872,14 +1898,31 @@ class Model {
|
|
|
872
1898
|
}
|
|
873
1899
|
function registerModelRepository(model, repository) {
|
|
874
1900
|
modelRepositories.set(model, repository);
|
|
1901
|
+
const name = model.name;
|
|
1902
|
+
if (name) {
|
|
1903
|
+
namedModels.set(name, model);
|
|
1904
|
+
}
|
|
1905
|
+
const morphClass = model.$morphClass;
|
|
1906
|
+
if (morphClass) {
|
|
1907
|
+
namedModels.set(morphClass, model);
|
|
1908
|
+
}
|
|
875
1909
|
ensureBooted(model);
|
|
876
1910
|
return model;
|
|
877
1911
|
}
|
|
878
1912
|
export {
|
|
1913
|
+
BelongsToManyRelationQuery,
|
|
1914
|
+
BelongsToRelationQuery,
|
|
1915
|
+
HasManyRelationQuery,
|
|
1916
|
+
HasOneRelationQuery,
|
|
879
1917
|
Model,
|
|
1918
|
+
ModelQuery,
|
|
1919
|
+
MorphManyRelationQuery,
|
|
1920
|
+
MorphOneRelationQuery,
|
|
1921
|
+
MorphToRelationQuery,
|
|
880
1922
|
applyCasts,
|
|
881
1923
|
dehydrateValue,
|
|
882
1924
|
filterMassAssignable,
|
|
883
1925
|
hydrateValue,
|
|
1926
|
+
registerModelClass,
|
|
884
1927
|
registerModelRepository
|
|
885
1928
|
};
|