@veap/database 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +176 -2
- package/dist/casts.d.ts +9 -0
- package/dist/casts.js +78 -0
- package/dist/connection.d.ts +49 -0
- package/dist/connection.js +189 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.js +17 -0
- package/dist/events.d.ts +5 -0
- package/dist/events.js +7 -0
- package/dist/factory.d.ts +68 -0
- package/dist/factory.js +161 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +17 -0
- package/dist/logger.d.ts +11 -0
- package/dist/logger.js +21 -0
- package/dist/migration-provider.d.ts +5 -0
- package/dist/migration-provider.js +15 -0
- package/dist/migrations/generator.d.ts +4 -0
- package/dist/migrations/generator.js +58 -0
- package/dist/migrations/index.d.ts +3 -0
- package/dist/migrations/index.js +3 -0
- package/dist/migrations/runner.d.ts +12 -0
- package/dist/migrations/runner.js +178 -0
- package/dist/migrations/schema-builder.d.ts +84 -0
- package/dist/migrations/schema-builder.js +409 -0
- package/dist/model.d.ts +379 -0
- package/dist/model.js +899 -0
- package/dist/provider.d.ts +12 -0
- package/dist/provider.js +58 -0
- package/dist/query-builder.d.ts +197 -0
- package/dist/query-builder.js +715 -0
- package/dist/relations/belongs-to-many.d.ts +15 -0
- package/dist/relations/belongs-to-many.js +80 -0
- package/dist/relations/belongs-to.d.ts +8 -0
- package/dist/relations/belongs-to.js +37 -0
- package/dist/relations/eager-loader.d.ts +5 -0
- package/dist/relations/eager-loader.js +66 -0
- package/dist/relations/has-many.d.ts +9 -0
- package/dist/relations/has-many.js +48 -0
- package/dist/relations/has-one.d.ts +8 -0
- package/dist/relations/has-one.js +37 -0
- package/dist/relations/index.d.ts +11 -0
- package/dist/relations/index.js +11 -0
- package/dist/relations/morph-many.d.ts +14 -0
- package/dist/relations/morph-many.js +65 -0
- package/dist/relations/morph-map.d.ts +32 -0
- package/dist/relations/morph-map.js +73 -0
- package/dist/relations/morph-one.d.ts +14 -0
- package/dist/relations/morph-one.js +65 -0
- package/dist/relations/morph-to-many.d.ts +44 -0
- package/dist/relations/morph-to-many.js +215 -0
- package/dist/relations/morph-to.d.ts +19 -0
- package/dist/relations/morph-to.js +90 -0
- package/dist/relations/relation.d.ts +22 -0
- package/dist/relations/relation.js +13 -0
- package/dist/scopes.d.ts +16 -0
- package/dist/scopes.js +1 -0
- package/dist/seeder.d.ts +25 -0
- package/dist/seeder.js +49 -0
- package/dist/types.d.ts +31 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +9 -0
- package/dist/utils.js +20 -0
- package/package.json +50 -4
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Model } from "../model";
|
|
2
|
+
import type { ModelQueryBuilder } from "../query-builder";
|
|
3
|
+
import { Relation } from "./relation";
|
|
4
|
+
export declare class BelongsToMany<Parent extends Model = any, Related extends Model = any> extends Relation<Parent, Related> {
|
|
5
|
+
pivotTable: string;
|
|
6
|
+
foreignPivotKey: string;
|
|
7
|
+
relatedPivotKey: string;
|
|
8
|
+
relatedKey: string;
|
|
9
|
+
constructor(parent: Parent, related: any, pivotTable: string, foreignPivotKey: string, relatedPivotKey: string, localKey?: string, relatedKey?: string);
|
|
10
|
+
query(): ModelQueryBuilder<Related>;
|
|
11
|
+
get(): Promise<Related[]>;
|
|
12
|
+
attach(relatedId: any, extraAttributes?: Record<string, any>): Promise<void>;
|
|
13
|
+
detach(relatedId?: any): Promise<void>;
|
|
14
|
+
match(parents: Parent[], relationName: string): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { getKnex } from "../connection";
|
|
2
|
+
import { Relation } from "./relation";
|
|
3
|
+
export class BelongsToMany extends Relation {
|
|
4
|
+
pivotTable;
|
|
5
|
+
foreignPivotKey;
|
|
6
|
+
relatedPivotKey;
|
|
7
|
+
relatedKey;
|
|
8
|
+
constructor(parent, related, pivotTable, foreignPivotKey, relatedPivotKey, localKey = "id", relatedKey = "id") {
|
|
9
|
+
super(parent, related, foreignPivotKey, localKey);
|
|
10
|
+
this.pivotTable = pivotTable;
|
|
11
|
+
this.foreignPivotKey = foreignPivotKey;
|
|
12
|
+
this.relatedPivotKey = relatedPivotKey;
|
|
13
|
+
this.relatedKey = relatedKey;
|
|
14
|
+
}
|
|
15
|
+
query() {
|
|
16
|
+
const localKeyValue = this.parent.getAttribute(this.localKey);
|
|
17
|
+
const relatedTable = this.related.table;
|
|
18
|
+
return this.related
|
|
19
|
+
.query()
|
|
20
|
+
.join(this.pivotTable, `${relatedTable}.${this.relatedKey}`, `${this.pivotTable}.${this.relatedPivotKey}`)
|
|
21
|
+
.where(`${this.pivotTable}.${this.foreignPivotKey}`, localKeyValue);
|
|
22
|
+
}
|
|
23
|
+
async get() {
|
|
24
|
+
const localKeyValue = this.parent.getAttribute(this.localKey);
|
|
25
|
+
if (localKeyValue === null || localKeyValue === undefined) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
return await this.query().get();
|
|
29
|
+
}
|
|
30
|
+
async attach(relatedId, extraAttributes = {}) {
|
|
31
|
+
const knex = getKnex();
|
|
32
|
+
const localKeyValue = this.parent.getAttribute(this.localKey);
|
|
33
|
+
await knex(this.pivotTable).insert({
|
|
34
|
+
[this.foreignPivotKey]: localKeyValue,
|
|
35
|
+
[this.relatedPivotKey]: relatedId,
|
|
36
|
+
...extraAttributes,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
async detach(relatedId) {
|
|
40
|
+
const knex = getKnex();
|
|
41
|
+
const localKeyValue = this.parent.getAttribute(this.localKey);
|
|
42
|
+
let q = knex(this.pivotTable).where(this.foreignPivotKey, localKeyValue);
|
|
43
|
+
if (relatedId !== undefined) {
|
|
44
|
+
q = q.where(this.relatedPivotKey, relatedId);
|
|
45
|
+
}
|
|
46
|
+
await q.delete();
|
|
47
|
+
}
|
|
48
|
+
async match(parents, relationName) {
|
|
49
|
+
const localKeys = Array.from(new Set(parents
|
|
50
|
+
.map((p) => p.getAttribute(this.localKey))
|
|
51
|
+
.filter((key) => key !== null && key !== undefined)));
|
|
52
|
+
if (localKeys.length === 0) {
|
|
53
|
+
for (const parent of parents) {
|
|
54
|
+
parent.setRelation(relationName, []);
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const knex = getKnex();
|
|
59
|
+
const relatedTable = this.related.table;
|
|
60
|
+
// Fetch related records joined with the pivot table so we can associate by foreignPivotKey
|
|
61
|
+
const rows = await knex(relatedTable)
|
|
62
|
+
.join(this.pivotTable, `${relatedTable}.${this.relatedKey}`, `${this.pivotTable}.${this.relatedPivotKey}`)
|
|
63
|
+
.whereIn(`${this.pivotTable}.${this.foreignPivotKey}`, localKeys)
|
|
64
|
+
.select(`${relatedTable}.*`, `${this.pivotTable}.${this.foreignPivotKey} as _pivot_foreign_key`);
|
|
65
|
+
const lookup = new Map();
|
|
66
|
+
for (const row of rows) {
|
|
67
|
+
const parentFk = row._pivot_foreign_key;
|
|
68
|
+
delete row._pivot_foreign_key;
|
|
69
|
+
const model = new this.related(row, false);
|
|
70
|
+
if (!lookup.has(parentFk)) {
|
|
71
|
+
lookup.set(parentFk, []);
|
|
72
|
+
}
|
|
73
|
+
lookup.get(parentFk).push(model);
|
|
74
|
+
}
|
|
75
|
+
for (const parent of parents) {
|
|
76
|
+
const lk = parent.getAttribute(this.localKey);
|
|
77
|
+
parent.setRelation(relationName, lookup.get(lk) || []);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Model } from "../model";
|
|
2
|
+
import type { ModelQueryBuilder } from "../query-builder";
|
|
3
|
+
import { Relation } from "./relation";
|
|
4
|
+
export declare class BelongsTo<Parent extends Model = any, Related extends Model = any> extends Relation<Parent, Related> {
|
|
5
|
+
query(): ModelQueryBuilder<Related>;
|
|
6
|
+
get(): Promise<Related | null>;
|
|
7
|
+
match(parents: Parent[], relationName: string): Promise<void>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Relation } from "./relation";
|
|
2
|
+
export class BelongsTo extends Relation {
|
|
3
|
+
query() {
|
|
4
|
+
const foreignKeyValue = this.parent.getAttribute(this.foreignKey);
|
|
5
|
+
return this.related.query().where(this.localKey, foreignKeyValue);
|
|
6
|
+
}
|
|
7
|
+
async get() {
|
|
8
|
+
const foreignKeyValue = this.parent.getAttribute(this.foreignKey);
|
|
9
|
+
if (foreignKeyValue === null || foreignKeyValue === undefined) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
return await this.query().first();
|
|
13
|
+
}
|
|
14
|
+
async match(parents, relationName) {
|
|
15
|
+
const foreignKeys = Array.from(new Set(parents
|
|
16
|
+
.map((p) => p.getAttribute(this.foreignKey))
|
|
17
|
+
.filter((key) => key !== null && key !== undefined)));
|
|
18
|
+
if (foreignKeys.length === 0) {
|
|
19
|
+
for (const parent of parents) {
|
|
20
|
+
parent.setRelation(relationName, null);
|
|
21
|
+
}
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const results = await this.related
|
|
25
|
+
.query()
|
|
26
|
+
.whereIn(this.localKey, foreignKeys)
|
|
27
|
+
.get();
|
|
28
|
+
const lookup = new Map();
|
|
29
|
+
for (const item of results) {
|
|
30
|
+
lookup.set(item.getAttribute(this.localKey), item);
|
|
31
|
+
}
|
|
32
|
+
for (const parent of parents) {
|
|
33
|
+
const fk = parent.getAttribute(this.foreignKey);
|
|
34
|
+
parent.setRelation(relationName, lookup.get(fk) || null);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { DatabaseError } from "../errors";
|
|
2
|
+
/**
|
|
3
|
+
* Loads relations onto an array of model instances in bulk queries, avoiding N+1.
|
|
4
|
+
*/
|
|
5
|
+
export async function eagerLoadRelations(models, relations) {
|
|
6
|
+
if (models.length === 0 || relations.length === 0) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
// Parse nested relations, e.g. "posts.comments" -> { posts: ["comments"] }
|
|
10
|
+
const tree = {};
|
|
11
|
+
for (const rel of relations) {
|
|
12
|
+
const parts = rel.split(".");
|
|
13
|
+
const head = parts[0];
|
|
14
|
+
const tail = parts.slice(1).join(".");
|
|
15
|
+
if (!tree[head]) {
|
|
16
|
+
tree[head] = [];
|
|
17
|
+
}
|
|
18
|
+
if (tail) {
|
|
19
|
+
tree[head].push(tail);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const sample = models[0];
|
|
23
|
+
for (const [relationName, nestedRels] of Object.entries(tree)) {
|
|
24
|
+
// Resolve relation instance from model (handling cases where loaded relation data shadows the method)
|
|
25
|
+
let relationInstance = null;
|
|
26
|
+
if (typeof sample.getRelationDefinition === "function") {
|
|
27
|
+
relationInstance = sample.getRelationDefinition(relationName);
|
|
28
|
+
}
|
|
29
|
+
if (!relationInstance) {
|
|
30
|
+
if (typeof sample[relationName] === "function") {
|
|
31
|
+
relationInstance = sample[relationName]();
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
let proto = Object.getPrototypeOf(sample);
|
|
35
|
+
while (proto && proto !== Object.prototype) {
|
|
36
|
+
const desc = Object.getOwnPropertyDescriptor(proto, relationName);
|
|
37
|
+
if (desc && typeof desc.value === "function") {
|
|
38
|
+
relationInstance = desc.value.call(sample);
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
proto = Object.getPrototypeOf(proto);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (!relationInstance) {
|
|
46
|
+
throw DatabaseError.Internal(`[veap:ORM] Relation "${relationName}" is not defined on model ${sample.constructor.name}.`);
|
|
47
|
+
}
|
|
48
|
+
await relationInstance.match(models, relationName);
|
|
49
|
+
// If there are nested relations, recursively eager load them on the children
|
|
50
|
+
if (nestedRels.length > 0) {
|
|
51
|
+
const allChildren = [];
|
|
52
|
+
for (const parent of models) {
|
|
53
|
+
const childOrChildren = parent.getRelation(relationName);
|
|
54
|
+
if (Array.isArray(childOrChildren)) {
|
|
55
|
+
allChildren.push(...childOrChildren);
|
|
56
|
+
}
|
|
57
|
+
else if (childOrChildren) {
|
|
58
|
+
allChildren.push(childOrChildren);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (allChildren.length > 0) {
|
|
62
|
+
await eagerLoadRelations(allChildren, nestedRels);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Model } from "../model";
|
|
2
|
+
import type { ModelQueryBuilder } from "../query-builder";
|
|
3
|
+
import { Relation } from "./relation";
|
|
4
|
+
export declare class HasMany<Parent extends Model = any, Related extends Model = any> extends Relation<Parent, Related> {
|
|
5
|
+
query(): ModelQueryBuilder<Related>;
|
|
6
|
+
get(): Promise<Related[]>;
|
|
7
|
+
create(attributes?: Record<string, any>): Promise<Related>;
|
|
8
|
+
match(parents: Parent[], relationName: string): Promise<void>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Relation } from "./relation";
|
|
2
|
+
export class HasMany extends Relation {
|
|
3
|
+
query() {
|
|
4
|
+
const localKeyValue = this.parent.getAttribute(this.localKey);
|
|
5
|
+
return this.related.query().where(this.foreignKey, localKeyValue);
|
|
6
|
+
}
|
|
7
|
+
async get() {
|
|
8
|
+
const localKeyValue = this.parent.getAttribute(this.localKey);
|
|
9
|
+
if (localKeyValue === null || localKeyValue === undefined) {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
return await this.query().get();
|
|
13
|
+
}
|
|
14
|
+
async create(attributes = {}) {
|
|
15
|
+
const localKeyValue = this.parent.getAttribute(this.localKey);
|
|
16
|
+
return await this.related.create({
|
|
17
|
+
...attributes,
|
|
18
|
+
[this.foreignKey]: localKeyValue,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
async match(parents, relationName) {
|
|
22
|
+
const localKeys = Array.from(new Set(parents
|
|
23
|
+
.map((p) => p.getAttribute(this.localKey))
|
|
24
|
+
.filter((key) => key !== null && key !== undefined)));
|
|
25
|
+
if (localKeys.length === 0) {
|
|
26
|
+
for (const parent of parents) {
|
|
27
|
+
parent.setRelation(relationName, []);
|
|
28
|
+
}
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const results = await this.related
|
|
32
|
+
.query()
|
|
33
|
+
.whereIn(this.foreignKey, localKeys)
|
|
34
|
+
.get();
|
|
35
|
+
const lookup = new Map();
|
|
36
|
+
for (const item of results) {
|
|
37
|
+
const fk = item.getAttribute(this.foreignKey);
|
|
38
|
+
if (!lookup.has(fk)) {
|
|
39
|
+
lookup.set(fk, []);
|
|
40
|
+
}
|
|
41
|
+
lookup.get(fk).push(item);
|
|
42
|
+
}
|
|
43
|
+
for (const parent of parents) {
|
|
44
|
+
const lk = parent.getAttribute(this.localKey);
|
|
45
|
+
parent.setRelation(relationName, lookup.get(lk) || []);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Model } from "../model";
|
|
2
|
+
import type { ModelQueryBuilder } from "../query-builder";
|
|
3
|
+
import { Relation } from "./relation";
|
|
4
|
+
export declare class HasOne<Parent extends Model = any, Related extends Model = any> extends Relation<Parent, Related> {
|
|
5
|
+
query(): ModelQueryBuilder<Related>;
|
|
6
|
+
get(): Promise<Related | null>;
|
|
7
|
+
match(parents: Parent[], relationName: string): Promise<void>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Relation } from "./relation";
|
|
2
|
+
export class HasOne extends Relation {
|
|
3
|
+
query() {
|
|
4
|
+
const localKeyValue = this.parent.getAttribute(this.localKey);
|
|
5
|
+
return this.related.query().where(this.foreignKey, localKeyValue);
|
|
6
|
+
}
|
|
7
|
+
async get() {
|
|
8
|
+
const localKeyValue = this.parent.getAttribute(this.localKey);
|
|
9
|
+
if (localKeyValue === null || localKeyValue === undefined) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
return await this.query().first();
|
|
13
|
+
}
|
|
14
|
+
async match(parents, relationName) {
|
|
15
|
+
const localKeys = Array.from(new Set(parents
|
|
16
|
+
.map((p) => p.getAttribute(this.localKey))
|
|
17
|
+
.filter((key) => key !== null && key !== undefined)));
|
|
18
|
+
if (localKeys.length === 0) {
|
|
19
|
+
for (const parent of parents) {
|
|
20
|
+
parent.setRelation(relationName, null);
|
|
21
|
+
}
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const results = await this.related
|
|
25
|
+
.query()
|
|
26
|
+
.whereIn(this.foreignKey, localKeys)
|
|
27
|
+
.get();
|
|
28
|
+
const lookup = new Map();
|
|
29
|
+
for (const item of results) {
|
|
30
|
+
lookup.set(item.getAttribute(this.foreignKey), item);
|
|
31
|
+
}
|
|
32
|
+
for (const parent of parents) {
|
|
33
|
+
const lk = parent.getAttribute(this.localKey);
|
|
34
|
+
parent.setRelation(relationName, lookup.get(lk) || null);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from "./belongs-to";
|
|
2
|
+
export * from "./belongs-to-many";
|
|
3
|
+
export * from "./eager-loader";
|
|
4
|
+
export * from "./has-many";
|
|
5
|
+
export * from "./has-one";
|
|
6
|
+
export * from "./morph-many";
|
|
7
|
+
export * from "./morph-map";
|
|
8
|
+
export * from "./morph-one";
|
|
9
|
+
export * from "./morph-to";
|
|
10
|
+
export * from "./morph-to-many";
|
|
11
|
+
export * from "./relation";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from "./belongs-to";
|
|
2
|
+
export * from "./belongs-to-many";
|
|
3
|
+
export * from "./eager-loader";
|
|
4
|
+
export * from "./has-many";
|
|
5
|
+
export * from "./has-one";
|
|
6
|
+
export * from "./morph-many";
|
|
7
|
+
export * from "./morph-map";
|
|
8
|
+
export * from "./morph-one";
|
|
9
|
+
export * from "./morph-to";
|
|
10
|
+
export * from "./morph-to-many";
|
|
11
|
+
export * from "./relation";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Model } from "../model";
|
|
2
|
+
import type { ModelQueryBuilder } from "../query-builder";
|
|
3
|
+
import { Relation } from "./relation";
|
|
4
|
+
export declare class MorphMany<Parent extends Model = any, Related extends Model = any> extends Relation<Parent, Related> {
|
|
5
|
+
morphName: string;
|
|
6
|
+
typeColumn: string;
|
|
7
|
+
idColumn: string;
|
|
8
|
+
morphType: string;
|
|
9
|
+
constructor(parent: Parent, related: any, morphName: string, typeColumn: string, idColumn: string, localKey?: string);
|
|
10
|
+
query(): ModelQueryBuilder<Related>;
|
|
11
|
+
get(): Promise<Related[]>;
|
|
12
|
+
create(attributes?: Record<string, any>): Promise<Related>;
|
|
13
|
+
match(parents: Parent[], relationName: string): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { MorphMap } from "./morph-map";
|
|
2
|
+
import { Relation } from "./relation";
|
|
3
|
+
export class MorphMany extends Relation {
|
|
4
|
+
morphName;
|
|
5
|
+
typeColumn;
|
|
6
|
+
idColumn;
|
|
7
|
+
morphType;
|
|
8
|
+
constructor(parent, related, morphName, typeColumn, idColumn, localKey = parent.constructor.primaryKey || "id") {
|
|
9
|
+
super(parent, related, idColumn, localKey);
|
|
10
|
+
this.morphName = morphName;
|
|
11
|
+
this.typeColumn = typeColumn;
|
|
12
|
+
this.idColumn = idColumn;
|
|
13
|
+
this.morphType = MorphMap.getMorphAlias(parent);
|
|
14
|
+
}
|
|
15
|
+
query() {
|
|
16
|
+
const localKeyValue = this.parent.getAttribute(this.localKey);
|
|
17
|
+
return this.related
|
|
18
|
+
.query()
|
|
19
|
+
.where(this.typeColumn, this.morphType)
|
|
20
|
+
.where(this.idColumn, localKeyValue);
|
|
21
|
+
}
|
|
22
|
+
async get() {
|
|
23
|
+
const localKeyValue = this.parent.getAttribute(this.localKey);
|
|
24
|
+
if (localKeyValue === null || localKeyValue === undefined) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
return await this.query().get();
|
|
28
|
+
}
|
|
29
|
+
async create(attributes = {}) {
|
|
30
|
+
const localKeyValue = this.parent.getAttribute(this.localKey);
|
|
31
|
+
return await this.related.create({
|
|
32
|
+
...attributes,
|
|
33
|
+
[this.typeColumn]: this.morphType,
|
|
34
|
+
[this.idColumn]: localKeyValue,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
async match(parents, relationName) {
|
|
38
|
+
const localKeys = Array.from(new Set(parents
|
|
39
|
+
.map((p) => p.getAttribute(this.localKey))
|
|
40
|
+
.filter((key) => key !== null && key !== undefined)));
|
|
41
|
+
if (localKeys.length === 0) {
|
|
42
|
+
for (const parent of parents) {
|
|
43
|
+
parent.setRelation(relationName, []);
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const results = await this.related
|
|
48
|
+
.query()
|
|
49
|
+
.where(this.typeColumn, this.morphType)
|
|
50
|
+
.whereIn(this.idColumn, localKeys)
|
|
51
|
+
.get();
|
|
52
|
+
const lookup = new Map();
|
|
53
|
+
for (const item of results) {
|
|
54
|
+
const fk = item.getAttribute(this.idColumn);
|
|
55
|
+
if (!lookup.has(fk)) {
|
|
56
|
+
lookup.set(fk, []);
|
|
57
|
+
}
|
|
58
|
+
lookup.get(fk).push(item);
|
|
59
|
+
}
|
|
60
|
+
for (const parent of parents) {
|
|
61
|
+
const lk = parent.getAttribute(this.localKey);
|
|
62
|
+
parent.setRelation(relationName, lookup.get(lk) || []);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry storing polymorphic type aliases mapped to Model constructors and vice-versa.
|
|
3
|
+
*/
|
|
4
|
+
export declare class MorphMap {
|
|
5
|
+
private static get aliasToModel();
|
|
6
|
+
private static get modelToAlias();
|
|
7
|
+
/**
|
|
8
|
+
* Registers a mapping of aliases to Model constructors.
|
|
9
|
+
*
|
|
10
|
+
* Example:
|
|
11
|
+
* MorphMap.register({
|
|
12
|
+
* post: BlogPost,
|
|
13
|
+
* user: User,
|
|
14
|
+
* comment: BlogComment,
|
|
15
|
+
* });
|
|
16
|
+
*/
|
|
17
|
+
static register(map: Record<string, any>): void;
|
|
18
|
+
/**
|
|
19
|
+
* Gets the Model constructor registered for a given morph alias.
|
|
20
|
+
* If not registered in morphMap, attempts to return the input if it's already a class.
|
|
21
|
+
*/
|
|
22
|
+
static getModel(aliasOrName: string): any;
|
|
23
|
+
/**
|
|
24
|
+
* Resolves the polymorphic string type for a model class or instance.
|
|
25
|
+
* Returns the registered morph alias if exists, otherwise falls back to model table name or class name.
|
|
26
|
+
*/
|
|
27
|
+
static getMorphAlias(modelClassOrInstance: any): string;
|
|
28
|
+
/**
|
|
29
|
+
* Clears the morph map registry (mainly used for testing).
|
|
30
|
+
*/
|
|
31
|
+
static clear(): void;
|
|
32
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const globalForMorph = globalThis;
|
|
2
|
+
/**
|
|
3
|
+
* Registry storing polymorphic type aliases mapped to Model constructors and vice-versa.
|
|
4
|
+
*/
|
|
5
|
+
export class MorphMap {
|
|
6
|
+
static get aliasToModel() {
|
|
7
|
+
if (!globalForMorph.__VEAP_MORPH_ALIAS_TO_MODEL__) {
|
|
8
|
+
globalForMorph.__VEAP_MORPH_ALIAS_TO_MODEL__ = new Map();
|
|
9
|
+
}
|
|
10
|
+
return globalForMorph.__VEAP_MORPH_ALIAS_TO_MODEL__;
|
|
11
|
+
}
|
|
12
|
+
static get modelToAlias() {
|
|
13
|
+
if (!globalForMorph.__VEAP_MORPH_MODEL_TO_ALIAS__) {
|
|
14
|
+
globalForMorph.__VEAP_MORPH_MODEL_TO_ALIAS__ = new Map();
|
|
15
|
+
}
|
|
16
|
+
return globalForMorph.__VEAP_MORPH_MODEL_TO_ALIAS__;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Registers a mapping of aliases to Model constructors.
|
|
20
|
+
*
|
|
21
|
+
* Example:
|
|
22
|
+
* MorphMap.register({
|
|
23
|
+
* post: BlogPost,
|
|
24
|
+
* user: User,
|
|
25
|
+
* comment: BlogComment,
|
|
26
|
+
* });
|
|
27
|
+
*/
|
|
28
|
+
static register(map) {
|
|
29
|
+
for (const [alias, modelClass] of Object.entries(map)) {
|
|
30
|
+
this.aliasToModel.set(alias, modelClass);
|
|
31
|
+
if (!this.modelToAlias.has(modelClass)) {
|
|
32
|
+
this.modelToAlias.set(modelClass, alias);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Gets the Model constructor registered for a given morph alias.
|
|
38
|
+
* If not registered in morphMap, attempts to return the input if it's already a class.
|
|
39
|
+
*/
|
|
40
|
+
static getModel(aliasOrName) {
|
|
41
|
+
return this.aliasToModel.get(aliasOrName) || null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Resolves the polymorphic string type for a model class or instance.
|
|
45
|
+
* Returns the registered morph alias if exists, otherwise falls back to model table name or class name.
|
|
46
|
+
*/
|
|
47
|
+
static getMorphAlias(modelClassOrInstance) {
|
|
48
|
+
const ctor = typeof modelClassOrInstance === "function"
|
|
49
|
+
? modelClassOrInstance
|
|
50
|
+
: modelClassOrInstance?.constructor;
|
|
51
|
+
if (ctor && this.modelToAlias.has(ctor)) {
|
|
52
|
+
return this.modelToAlias.get(ctor);
|
|
53
|
+
}
|
|
54
|
+
if (ctor?.morphAlias) {
|
|
55
|
+
this.register({ [ctor.morphAlias]: ctor });
|
|
56
|
+
return ctor.morphAlias;
|
|
57
|
+
}
|
|
58
|
+
if (ctor?.table) {
|
|
59
|
+
return ctor.table;
|
|
60
|
+
}
|
|
61
|
+
if (ctor?.name) {
|
|
62
|
+
return ctor.name;
|
|
63
|
+
}
|
|
64
|
+
return String(modelClassOrInstance);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Clears the morph map registry (mainly used for testing).
|
|
68
|
+
*/
|
|
69
|
+
static clear() {
|
|
70
|
+
this.aliasToModel.clear();
|
|
71
|
+
this.modelToAlias.clear();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Model } from "../model";
|
|
2
|
+
import type { ModelQueryBuilder } from "../query-builder";
|
|
3
|
+
import { Relation } from "./relation";
|
|
4
|
+
export declare class MorphOne<Parent extends Model = any, Related extends Model = any> extends Relation<Parent, Related> {
|
|
5
|
+
morphName: string;
|
|
6
|
+
typeColumn: string;
|
|
7
|
+
idColumn: string;
|
|
8
|
+
morphType: string;
|
|
9
|
+
constructor(parent: Parent, related: any, morphName: string, typeColumn: string, idColumn: string, localKey?: string);
|
|
10
|
+
query(): ModelQueryBuilder<Related>;
|
|
11
|
+
get(): Promise<Related | null>;
|
|
12
|
+
create(attributes?: Record<string, any>): Promise<Related>;
|
|
13
|
+
match(parents: Parent[], relationName: string): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { MorphMap } from "./morph-map";
|
|
2
|
+
import { Relation } from "./relation";
|
|
3
|
+
export class MorphOne extends Relation {
|
|
4
|
+
morphName;
|
|
5
|
+
typeColumn;
|
|
6
|
+
idColumn;
|
|
7
|
+
morphType;
|
|
8
|
+
constructor(parent, related, morphName, typeColumn, idColumn, localKey = parent.constructor.primaryKey || "id") {
|
|
9
|
+
super(parent, related, idColumn, localKey);
|
|
10
|
+
this.morphName = morphName;
|
|
11
|
+
this.typeColumn = typeColumn;
|
|
12
|
+
this.idColumn = idColumn;
|
|
13
|
+
this.morphType = MorphMap.getMorphAlias(parent);
|
|
14
|
+
}
|
|
15
|
+
query() {
|
|
16
|
+
const localKeyValue = this.parent.getAttribute(this.localKey);
|
|
17
|
+
return this.related
|
|
18
|
+
.query()
|
|
19
|
+
.where(this.typeColumn, this.morphType)
|
|
20
|
+
.where(this.idColumn, localKeyValue);
|
|
21
|
+
}
|
|
22
|
+
async get() {
|
|
23
|
+
const localKeyValue = this.parent.getAttribute(this.localKey);
|
|
24
|
+
if (localKeyValue === null || localKeyValue === undefined) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const result = await this.query().first();
|
|
28
|
+
return result || null;
|
|
29
|
+
}
|
|
30
|
+
async create(attributes = {}) {
|
|
31
|
+
const localKeyValue = this.parent.getAttribute(this.localKey);
|
|
32
|
+
return await this.related.create({
|
|
33
|
+
...attributes,
|
|
34
|
+
[this.typeColumn]: this.morphType,
|
|
35
|
+
[this.idColumn]: localKeyValue,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
async match(parents, relationName) {
|
|
39
|
+
const localKeys = Array.from(new Set(parents
|
|
40
|
+
.map((p) => p.getAttribute(this.localKey))
|
|
41
|
+
.filter((key) => key !== null && key !== undefined)));
|
|
42
|
+
if (localKeys.length === 0) {
|
|
43
|
+
for (const parent of parents) {
|
|
44
|
+
parent.setRelation(relationName, null);
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const results = await this.related
|
|
49
|
+
.query()
|
|
50
|
+
.where(this.typeColumn, this.morphType)
|
|
51
|
+
.whereIn(this.idColumn, localKeys)
|
|
52
|
+
.get();
|
|
53
|
+
const lookup = new Map();
|
|
54
|
+
for (const item of results) {
|
|
55
|
+
const fk = item.getAttribute(this.idColumn);
|
|
56
|
+
if (!lookup.has(fk)) {
|
|
57
|
+
lookup.set(fk, item);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
for (const parent of parents) {
|
|
61
|
+
const lk = parent.getAttribute(this.localKey);
|
|
62
|
+
parent.setRelation(relationName, lookup.get(lk) || null);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Model } from "../model";
|
|
2
|
+
import type { ModelQueryBuilder } from "../query-builder";
|
|
3
|
+
import { Relation } from "./relation";
|
|
4
|
+
/**
|
|
5
|
+
* MorphToMany represents a many-to-many polymorphic relation where the parent
|
|
6
|
+
* owns the polymorphic side of the pivot table (e.g. BlogPost -> tags via taggables).
|
|
7
|
+
* Pivot columns: ${morphName}_id, ${morphName}_type, ${relatedPivotKey}
|
|
8
|
+
*/
|
|
9
|
+
export declare class MorphToMany<Parent extends Model = any, Related extends Model = any> extends Relation<Parent, Related> {
|
|
10
|
+
morphName: string;
|
|
11
|
+
pivotTable: string;
|
|
12
|
+
foreignPivotKey: string;
|
|
13
|
+
relatedPivotKey: string;
|
|
14
|
+
morphTypeColumn: string;
|
|
15
|
+
relatedKey: string;
|
|
16
|
+
morphType: string;
|
|
17
|
+
constructor(parent: Parent, related: any, morphName: string, pivotTable: string, foreignPivotKey: string, relatedPivotKey: string, morphTypeColumn: string, localKey?: string, relatedKey?: string);
|
|
18
|
+
query(): ModelQueryBuilder<Related>;
|
|
19
|
+
get(): Promise<Related[]>;
|
|
20
|
+
attach(relatedId: any, extraAttributes?: Record<string, any>): Promise<void>;
|
|
21
|
+
detach(relatedId?: any): Promise<void>;
|
|
22
|
+
sync(ids: any[]): Promise<void>;
|
|
23
|
+
match(parents: Parent[], relationName: string): Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* MorphedByMany represents the inverse of MorphToMany (e.g. Tag -> posts via taggables).
|
|
27
|
+
* Pivot columns: ${relatedPivotKey} (tag_id), ${foreignPivotKey} (taggable_id), ${morphTypeColumn} (taggable_type)
|
|
28
|
+
*/
|
|
29
|
+
export declare class MorphedByMany<Parent extends Model = any, Related extends Model = any> extends Relation<Parent, Related> {
|
|
30
|
+
morphName: string;
|
|
31
|
+
pivotTable: string;
|
|
32
|
+
foreignPivotKey: string;
|
|
33
|
+
relatedPivotKey: string;
|
|
34
|
+
morphTypeColumn: string;
|
|
35
|
+
relatedKey: string;
|
|
36
|
+
morphType: string;
|
|
37
|
+
constructor(parent: Parent, related: any, morphName: string, pivotTable: string, foreignPivotKey: string, relatedPivotKey: string, morphTypeColumn: string, localKey?: string, relatedKey?: string);
|
|
38
|
+
query(): ModelQueryBuilder<Related>;
|
|
39
|
+
get(): Promise<Related[]>;
|
|
40
|
+
attach(relatedId: any, extraAttributes?: Record<string, any>): Promise<void>;
|
|
41
|
+
detach(relatedId?: any): Promise<void>;
|
|
42
|
+
sync(ids: any[]): Promise<void>;
|
|
43
|
+
match(parents: Parent[], relationName: string): Promise<void>;
|
|
44
|
+
}
|