@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,215 @@
|
|
|
1
|
+
import { getKnex } from "../connection";
|
|
2
|
+
import { MorphMap } from "./morph-map";
|
|
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 class MorphToMany extends Relation {
|
|
10
|
+
morphName;
|
|
11
|
+
pivotTable;
|
|
12
|
+
foreignPivotKey;
|
|
13
|
+
relatedPivotKey;
|
|
14
|
+
morphTypeColumn;
|
|
15
|
+
relatedKey;
|
|
16
|
+
morphType;
|
|
17
|
+
constructor(parent, related, morphName, pivotTable, foreignPivotKey, relatedPivotKey, morphTypeColumn, localKey = parent.constructor.primaryKey || "id", relatedKey = related.primaryKey || "id") {
|
|
18
|
+
super(parent, related, foreignPivotKey, localKey);
|
|
19
|
+
this.morphName = morphName;
|
|
20
|
+
this.pivotTable = pivotTable;
|
|
21
|
+
this.foreignPivotKey = foreignPivotKey;
|
|
22
|
+
this.relatedPivotKey = relatedPivotKey;
|
|
23
|
+
this.morphTypeColumn = morphTypeColumn;
|
|
24
|
+
this.relatedKey = relatedKey;
|
|
25
|
+
this.morphType = MorphMap.getMorphAlias(parent);
|
|
26
|
+
}
|
|
27
|
+
query() {
|
|
28
|
+
const localKeyValue = this.parent.getAttribute(this.localKey);
|
|
29
|
+
const relatedTable = this.related.table;
|
|
30
|
+
return this.related
|
|
31
|
+
.query()
|
|
32
|
+
.join(this.pivotTable, `${relatedTable}.${this.relatedKey}`, `${this.pivotTable}.${this.relatedPivotKey}`)
|
|
33
|
+
.where(`${this.pivotTable}.${this.foreignPivotKey}`, localKeyValue)
|
|
34
|
+
.where(`${this.pivotTable}.${this.morphTypeColumn}`, this.morphType);
|
|
35
|
+
}
|
|
36
|
+
async get() {
|
|
37
|
+
const localKeyValue = this.parent.getAttribute(this.localKey);
|
|
38
|
+
if (localKeyValue === null || localKeyValue === undefined) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
return await this.query().get();
|
|
42
|
+
}
|
|
43
|
+
async attach(relatedId, extraAttributes = {}) {
|
|
44
|
+
const knex = getKnex();
|
|
45
|
+
const localKeyValue = this.parent.getAttribute(this.localKey);
|
|
46
|
+
const ids = Array.isArray(relatedId) ? relatedId : [relatedId];
|
|
47
|
+
const rows = ids.map((id) => ({
|
|
48
|
+
[this.foreignPivotKey]: localKeyValue,
|
|
49
|
+
[this.morphTypeColumn]: this.morphType,
|
|
50
|
+
[this.relatedPivotKey]: id,
|
|
51
|
+
...extraAttributes,
|
|
52
|
+
}));
|
|
53
|
+
await knex(this.pivotTable).insert(rows);
|
|
54
|
+
}
|
|
55
|
+
async detach(relatedId) {
|
|
56
|
+
const knex = getKnex();
|
|
57
|
+
const localKeyValue = this.parent.getAttribute(this.localKey);
|
|
58
|
+
let q = knex(this.pivotTable)
|
|
59
|
+
.where(this.foreignPivotKey, localKeyValue)
|
|
60
|
+
.where(this.morphTypeColumn, this.morphType);
|
|
61
|
+
if (relatedId !== undefined) {
|
|
62
|
+
if (Array.isArray(relatedId)) {
|
|
63
|
+
q = q.whereIn(this.relatedPivotKey, relatedId);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
q = q.where(this.relatedPivotKey, relatedId);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
await q.delete();
|
|
70
|
+
}
|
|
71
|
+
async sync(ids) {
|
|
72
|
+
await this.detach();
|
|
73
|
+
if (ids.length > 0) {
|
|
74
|
+
await this.attach(ids);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async match(parents, relationName) {
|
|
78
|
+
const localKeys = Array.from(new Set(parents
|
|
79
|
+
.map((p) => p.getAttribute(this.localKey))
|
|
80
|
+
.filter((key) => key !== null && key !== undefined)));
|
|
81
|
+
if (localKeys.length === 0) {
|
|
82
|
+
for (const parent of parents) {
|
|
83
|
+
parent.setRelation(relationName, []);
|
|
84
|
+
}
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const knex = getKnex();
|
|
88
|
+
const relatedTable = this.related.table;
|
|
89
|
+
const rows = await knex(relatedTable)
|
|
90
|
+
.join(this.pivotTable, `${relatedTable}.${this.relatedKey}`, `${this.pivotTable}.${this.relatedPivotKey}`)
|
|
91
|
+
.whereIn(`${this.pivotTable}.${this.foreignPivotKey}`, localKeys)
|
|
92
|
+
.where(`${this.pivotTable}.${this.morphTypeColumn}`, this.morphType)
|
|
93
|
+
.select(`${relatedTable}.*`, `${this.pivotTable}.${this.foreignPivotKey} as _pivot_foreign_key`);
|
|
94
|
+
const lookup = new Map();
|
|
95
|
+
for (const row of rows) {
|
|
96
|
+
const parentFk = row._pivot_foreign_key;
|
|
97
|
+
delete row._pivot_foreign_key;
|
|
98
|
+
const model = new this.related(row, false);
|
|
99
|
+
if (!lookup.has(parentFk)) {
|
|
100
|
+
lookup.set(parentFk, []);
|
|
101
|
+
}
|
|
102
|
+
lookup.get(parentFk).push(model);
|
|
103
|
+
}
|
|
104
|
+
for (const parent of parents) {
|
|
105
|
+
const lk = parent.getAttribute(this.localKey);
|
|
106
|
+
parent.setRelation(relationName, lookup.get(lk) || []);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* MorphedByMany represents the inverse of MorphToMany (e.g. Tag -> posts via taggables).
|
|
112
|
+
* Pivot columns: ${relatedPivotKey} (tag_id), ${foreignPivotKey} (taggable_id), ${morphTypeColumn} (taggable_type)
|
|
113
|
+
*/
|
|
114
|
+
export class MorphedByMany extends Relation {
|
|
115
|
+
morphName;
|
|
116
|
+
pivotTable;
|
|
117
|
+
foreignPivotKey;
|
|
118
|
+
relatedPivotKey;
|
|
119
|
+
morphTypeColumn;
|
|
120
|
+
relatedKey;
|
|
121
|
+
morphType;
|
|
122
|
+
constructor(parent, related, morphName, pivotTable, foreignPivotKey, relatedPivotKey, morphTypeColumn, localKey = parent.constructor.primaryKey || "id", relatedKey = related.primaryKey || "id") {
|
|
123
|
+
super(parent, related, foreignPivotKey, localKey);
|
|
124
|
+
this.morphName = morphName;
|
|
125
|
+
this.pivotTable = pivotTable;
|
|
126
|
+
this.foreignPivotKey = foreignPivotKey;
|
|
127
|
+
this.relatedPivotKey = relatedPivotKey;
|
|
128
|
+
this.morphTypeColumn = morphTypeColumn;
|
|
129
|
+
this.relatedKey = relatedKey;
|
|
130
|
+
// In inverse, the morphType is the target related model (e.g. "post")
|
|
131
|
+
this.morphType = MorphMap.getMorphAlias(related);
|
|
132
|
+
}
|
|
133
|
+
query() {
|
|
134
|
+
const localKeyValue = this.parent.getAttribute(this.localKey);
|
|
135
|
+
const relatedTable = this.related.table;
|
|
136
|
+
return this.related
|
|
137
|
+
.query()
|
|
138
|
+
.join(this.pivotTable, `${relatedTable}.${this.relatedKey}`, `${this.pivotTable}.${this.foreignPivotKey}`)
|
|
139
|
+
.where(`${this.pivotTable}.${this.relatedPivotKey}`, localKeyValue)
|
|
140
|
+
.where(`${this.pivotTable}.${this.morphTypeColumn}`, this.morphType);
|
|
141
|
+
}
|
|
142
|
+
async get() {
|
|
143
|
+
const localKeyValue = this.parent.getAttribute(this.localKey);
|
|
144
|
+
if (localKeyValue === null || localKeyValue === undefined) {
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
return await this.query().get();
|
|
148
|
+
}
|
|
149
|
+
async attach(relatedId, extraAttributes = {}) {
|
|
150
|
+
const knex = getKnex();
|
|
151
|
+
const localKeyValue = this.parent.getAttribute(this.localKey);
|
|
152
|
+
const ids = Array.isArray(relatedId) ? relatedId : [relatedId];
|
|
153
|
+
const rows = ids.map((id) => ({
|
|
154
|
+
[this.relatedPivotKey]: localKeyValue,
|
|
155
|
+
[this.morphTypeColumn]: this.morphType,
|
|
156
|
+
[this.foreignPivotKey]: id,
|
|
157
|
+
...extraAttributes,
|
|
158
|
+
}));
|
|
159
|
+
await knex(this.pivotTable).insert(rows);
|
|
160
|
+
}
|
|
161
|
+
async detach(relatedId) {
|
|
162
|
+
const knex = getKnex();
|
|
163
|
+
const localKeyValue = this.parent.getAttribute(this.localKey);
|
|
164
|
+
let q = knex(this.pivotTable)
|
|
165
|
+
.where(this.relatedPivotKey, localKeyValue)
|
|
166
|
+
.where(this.morphTypeColumn, this.morphType);
|
|
167
|
+
if (relatedId !== undefined) {
|
|
168
|
+
if (Array.isArray(relatedId)) {
|
|
169
|
+
q = q.whereIn(this.foreignPivotKey, relatedId);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
q = q.where(this.foreignPivotKey, relatedId);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
await q.delete();
|
|
176
|
+
}
|
|
177
|
+
async sync(ids) {
|
|
178
|
+
await this.detach();
|
|
179
|
+
if (ids.length > 0) {
|
|
180
|
+
await this.attach(ids);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async match(parents, relationName) {
|
|
184
|
+
const localKeys = Array.from(new Set(parents
|
|
185
|
+
.map((p) => p.getAttribute(this.localKey))
|
|
186
|
+
.filter((key) => key !== null && key !== undefined)));
|
|
187
|
+
if (localKeys.length === 0) {
|
|
188
|
+
for (const parent of parents) {
|
|
189
|
+
parent.setRelation(relationName, []);
|
|
190
|
+
}
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const knex = getKnex();
|
|
194
|
+
const relatedTable = this.related.table;
|
|
195
|
+
const rows = await knex(relatedTable)
|
|
196
|
+
.join(this.pivotTable, `${relatedTable}.${this.relatedKey}`, `${this.pivotTable}.${this.foreignPivotKey}`)
|
|
197
|
+
.whereIn(`${this.pivotTable}.${this.relatedPivotKey}`, localKeys)
|
|
198
|
+
.where(`${this.pivotTable}.${this.morphTypeColumn}`, this.morphType)
|
|
199
|
+
.select(`${relatedTable}.*`, `${this.pivotTable}.${this.relatedPivotKey} as _pivot_parent_key`);
|
|
200
|
+
const lookup = new Map();
|
|
201
|
+
for (const row of rows) {
|
|
202
|
+
const parentFk = row._pivot_parent_key;
|
|
203
|
+
delete row._pivot_parent_key;
|
|
204
|
+
const model = new this.related(row, false);
|
|
205
|
+
if (!lookup.has(parentFk)) {
|
|
206
|
+
lookup.set(parentFk, []);
|
|
207
|
+
}
|
|
208
|
+
lookup.get(parentFk).push(model);
|
|
209
|
+
}
|
|
210
|
+
for (const parent of parents) {
|
|
211
|
+
const lk = parent.getAttribute(this.localKey);
|
|
212
|
+
parent.setRelation(relationName, lookup.get(lk) || []);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Model } from "../model";
|
|
2
|
+
import type { ModelQueryBuilder } from "../query-builder";
|
|
3
|
+
import { Relation } from "./relation";
|
|
4
|
+
export declare class MorphTo<Child extends Model = any, Parent extends Model = any> extends Relation<Child, Parent> {
|
|
5
|
+
morphName: string;
|
|
6
|
+
typeColumn: string;
|
|
7
|
+
idColumn: string;
|
|
8
|
+
constructor(parent: Child, morphName: string, typeColumn: string, idColumn: string);
|
|
9
|
+
/**
|
|
10
|
+
* Resolves the parent model class based on child's type column value.
|
|
11
|
+
*/
|
|
12
|
+
resolveModel(): any;
|
|
13
|
+
query(): ModelQueryBuilder<Parent>;
|
|
14
|
+
get(): Promise<Parent | null>;
|
|
15
|
+
/**
|
|
16
|
+
* Eager load polymorphic parents for multiple children in bulk queries grouped by morph type.
|
|
17
|
+
*/
|
|
18
|
+
match(children: Child[], relationName: string): Promise<void>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { DatabaseError } from "../errors";
|
|
2
|
+
import { MorphMap } from "./morph-map";
|
|
3
|
+
import { Relation } from "./relation";
|
|
4
|
+
export class MorphTo extends Relation {
|
|
5
|
+
morphName;
|
|
6
|
+
typeColumn;
|
|
7
|
+
idColumn;
|
|
8
|
+
constructor(parent, morphName, typeColumn, idColumn) {
|
|
9
|
+
// The related model class will be dynamically resolved from typeColumn
|
|
10
|
+
super(parent, null, idColumn, "id");
|
|
11
|
+
this.morphName = morphName;
|
|
12
|
+
this.typeColumn = typeColumn;
|
|
13
|
+
this.idColumn = idColumn;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Resolves the parent model class based on child's type column value.
|
|
17
|
+
*/
|
|
18
|
+
resolveModel() {
|
|
19
|
+
const typeValue = this.parent.getAttribute(this.typeColumn);
|
|
20
|
+
if (!typeValue) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const modelClass = MorphMap.getModel(typeValue);
|
|
24
|
+
if (!modelClass) {
|
|
25
|
+
throw DatabaseError.Internal(`[veap:ORM] Cannot resolve polymorphic model for morph type "${typeValue}". Did you register it with MorphMap.register()?`);
|
|
26
|
+
}
|
|
27
|
+
return modelClass;
|
|
28
|
+
}
|
|
29
|
+
query() {
|
|
30
|
+
const modelClass = this.resolveModel();
|
|
31
|
+
if (!modelClass) {
|
|
32
|
+
throw DatabaseError.Internal(`[veap:ORM] Cannot create query for morphTo relation "${this.morphName}" because type column "${this.typeColumn}" is null.`);
|
|
33
|
+
}
|
|
34
|
+
const idValue = this.parent.getAttribute(this.idColumn);
|
|
35
|
+
const pk = modelClass.primaryKey || "id";
|
|
36
|
+
return modelClass.query().where(pk, idValue);
|
|
37
|
+
}
|
|
38
|
+
async get() {
|
|
39
|
+
const modelClass = this.resolveModel();
|
|
40
|
+
const idValue = this.parent.getAttribute(this.idColumn);
|
|
41
|
+
if (!modelClass || !idValue) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const pk = modelClass.primaryKey || "id";
|
|
45
|
+
const result = await modelClass.query().where(pk, idValue).first();
|
|
46
|
+
return result || null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Eager load polymorphic parents for multiple children in bulk queries grouped by morph type.
|
|
50
|
+
*/
|
|
51
|
+
async match(children, relationName) {
|
|
52
|
+
// Group child records by morph type: typeValue -> ids
|
|
53
|
+
const typeToIds = new Map();
|
|
54
|
+
for (const child of children) {
|
|
55
|
+
const typeValue = child.getAttribute(this.typeColumn);
|
|
56
|
+
const idValue = child.getAttribute(this.idColumn);
|
|
57
|
+
if (typeValue && idValue) {
|
|
58
|
+
if (!typeToIds.has(typeValue)) {
|
|
59
|
+
typeToIds.set(typeValue, new Set());
|
|
60
|
+
}
|
|
61
|
+
typeToIds.get(typeValue).add(idValue);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Lookup map: `${typeValue}:${idValue}` -> Parent instance
|
|
65
|
+
const lookup = new Map();
|
|
66
|
+
// For each unique type, execute ONE bulk query (e.g. BlogPost.query().whereIn('id', ids))
|
|
67
|
+
for (const [typeValue, idSet] of typeToIds.entries()) {
|
|
68
|
+
const modelClass = MorphMap.getModel(typeValue);
|
|
69
|
+
if (!modelClass) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const pk = modelClass.primaryKey || "id";
|
|
73
|
+
const parents = await modelClass
|
|
74
|
+
.query()
|
|
75
|
+
.whereIn(pk, Array.from(idSet))
|
|
76
|
+
.get();
|
|
77
|
+
for (const parent of parents) {
|
|
78
|
+
const id = parent.getAttribute(pk);
|
|
79
|
+
lookup.set(`${typeValue}:${id}`, parent);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Attach resolved parent to each child
|
|
83
|
+
for (const child of children) {
|
|
84
|
+
const typeValue = child.getAttribute(this.typeColumn);
|
|
85
|
+
const idValue = child.getAttribute(this.idColumn);
|
|
86
|
+
const key = `${typeValue}:${idValue}`;
|
|
87
|
+
child.setRelation(relationName, lookup.get(key) || null);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Model } from "../model";
|
|
2
|
+
import type { ModelQueryBuilder } from "../query-builder";
|
|
3
|
+
export declare abstract class Relation<Parent extends Model = any, Related extends Model = any> {
|
|
4
|
+
parent: Parent;
|
|
5
|
+
related: any;
|
|
6
|
+
foreignKey: string;
|
|
7
|
+
localKey: string;
|
|
8
|
+
constructor(parent: Parent, related: any, // typeof Model
|
|
9
|
+
foreignKey: string, localKey: string);
|
|
10
|
+
/**
|
|
11
|
+
* Returns a new query builder pre-scoped for this relation.
|
|
12
|
+
*/
|
|
13
|
+
abstract query(): ModelQueryBuilder<Related>;
|
|
14
|
+
/**
|
|
15
|
+
* Fetches the related model(s) for the parent instance.
|
|
16
|
+
*/
|
|
17
|
+
abstract get(): Promise<any>;
|
|
18
|
+
/**
|
|
19
|
+
* Eager loads related models for an array of parents in a single bulk query (N+1 solver).
|
|
20
|
+
*/
|
|
21
|
+
abstract match(parents: Parent[], relationName: string): Promise<void>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export class Relation {
|
|
2
|
+
parent;
|
|
3
|
+
related;
|
|
4
|
+
foreignKey;
|
|
5
|
+
localKey;
|
|
6
|
+
constructor(parent, related, // typeof Model
|
|
7
|
+
foreignKey, localKey) {
|
|
8
|
+
this.parent = parent;
|
|
9
|
+
this.related = related;
|
|
10
|
+
this.foreignKey = foreignKey;
|
|
11
|
+
this.localKey = localKey;
|
|
12
|
+
}
|
|
13
|
+
}
|
package/dist/scopes.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Model } from "./model";
|
|
2
|
+
import type { ModelQueryBuilder } from "./query-builder";
|
|
3
|
+
/**
|
|
4
|
+
* Callback function for applying a global scope.
|
|
5
|
+
*/
|
|
6
|
+
export type GlobalScopeCallback<M extends Model = any> = (builder: ModelQueryBuilder<M>, modelClass: any) => void;
|
|
7
|
+
/**
|
|
8
|
+
* Interface for class-based global scopes.
|
|
9
|
+
*/
|
|
10
|
+
export interface Scope<M extends Model = any> {
|
|
11
|
+
apply(builder: ModelQueryBuilder<M>, modelClass: any): void;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Type representing either a callback or a Scope object.
|
|
15
|
+
*/
|
|
16
|
+
export type GlobalScope<M extends Model = any> = GlobalScopeCallback<M> | Scope<M>;
|
package/dist/scopes.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/seeder.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type SeederClass = new () => Seeder;
|
|
2
|
+
/**
|
|
3
|
+
* Base Database Seeder class.
|
|
4
|
+
*/
|
|
5
|
+
export declare abstract class Seeder {
|
|
6
|
+
/**
|
|
7
|
+
* Run the database seeds.
|
|
8
|
+
*/
|
|
9
|
+
abstract run(): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Run additional seeders sequentially.
|
|
12
|
+
*/
|
|
13
|
+
call(seeders: SeederClass[] | SeederClass): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export interface RunSeedersOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Whether to wrap all seeders execution inside a single database transaction.
|
|
18
|
+
* Defaults to false.
|
|
19
|
+
*/
|
|
20
|
+
inTransaction?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Runs one or multiple Seeders.
|
|
24
|
+
*/
|
|
25
|
+
export declare function runSeeders(seeders: SeederClass[] | SeederClass, options?: RunSeedersOptions): Promise<void>;
|
package/dist/seeder.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { transaction } from "./connection";
|
|
2
|
+
import { debug, info, warn } from "./logger";
|
|
3
|
+
/**
|
|
4
|
+
* Base Database Seeder class.
|
|
5
|
+
*/
|
|
6
|
+
export class Seeder {
|
|
7
|
+
/**
|
|
8
|
+
* Run additional seeders sequentially.
|
|
9
|
+
*/
|
|
10
|
+
async call(seeders) {
|
|
11
|
+
const list = Array.isArray(seeders) ? seeders : [seeders];
|
|
12
|
+
for (const SeederCtor of list) {
|
|
13
|
+
const seederName = SeederCtor.name || "Seeder";
|
|
14
|
+
info("veap:Seeder", `Running seeder: ${seederName}`);
|
|
15
|
+
const startTime = Date.now();
|
|
16
|
+
try {
|
|
17
|
+
const instance = new SeederCtor();
|
|
18
|
+
await instance.run();
|
|
19
|
+
const duration = Date.now() - startTime;
|
|
20
|
+
debug("veap:Seeder", `Completed seeder: ${seederName} (${duration}ms)`);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
warn("veap:Seeder", `Failed to run seeder: ${seederName}`, error);
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Runs one or multiple Seeders.
|
|
31
|
+
*/
|
|
32
|
+
export async function runSeeders(seeders, options = {}) {
|
|
33
|
+
const runner = async () => {
|
|
34
|
+
const rootSeeder = new (class extends Seeder {
|
|
35
|
+
async run() {
|
|
36
|
+
await this.call(seeders);
|
|
37
|
+
}
|
|
38
|
+
})();
|
|
39
|
+
await rootSeeder.run();
|
|
40
|
+
};
|
|
41
|
+
if (options.inTransaction) {
|
|
42
|
+
await transaction(async () => {
|
|
43
|
+
await runner();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
await runner();
|
|
48
|
+
}
|
|
49
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Knex } from "knex";
|
|
2
|
+
export interface DatabaseConfig extends Knex.Config {
|
|
3
|
+
driver?: "postgres" | "sqlite" | "mysql";
|
|
4
|
+
}
|
|
5
|
+
export interface PostgresSslOptions {
|
|
6
|
+
databaseUrl: string;
|
|
7
|
+
isProd: boolean;
|
|
8
|
+
rejectUnauthorizedEnv?: string;
|
|
9
|
+
caCert?: string;
|
|
10
|
+
}
|
|
11
|
+
export type PostgresSslConfig = false | {
|
|
12
|
+
rejectUnauthorized: boolean;
|
|
13
|
+
ca?: string;
|
|
14
|
+
};
|
|
15
|
+
export interface TransactionOptions {
|
|
16
|
+
/**
|
|
17
|
+
* When true and called inside an existing transaction, creates a SAVEPOINT
|
|
18
|
+
* instead of reusing the outer transaction directly. This allows catching
|
|
19
|
+
* errors in the inner transaction without aborting the outer transaction.
|
|
20
|
+
*
|
|
21
|
+
* @default false (Propagation: REQUIRED - reuses outer transaction)
|
|
22
|
+
*/
|
|
23
|
+
savepoint?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface PaginationResult<T> {
|
|
26
|
+
data: T[];
|
|
27
|
+
total: number;
|
|
28
|
+
perPage: number;
|
|
29
|
+
currentPage: number;
|
|
30
|
+
lastPage: number;
|
|
31
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts camelCase to snake_case.
|
|
3
|
+
* Handles dotted notation (e.g. 'table.columnName' -> 'table.column_name').
|
|
4
|
+
*/
|
|
5
|
+
export declare function toSnakeCase(str: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Converts snake_case to camelCase.
|
|
8
|
+
*/
|
|
9
|
+
export declare function toCamelCase(str: string): string;
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts camelCase to snake_case.
|
|
3
|
+
* Handles dotted notation (e.g. 'table.columnName' -> 'table.column_name').
|
|
4
|
+
*/
|
|
5
|
+
export function toSnakeCase(str) {
|
|
6
|
+
if (!str)
|
|
7
|
+
return str;
|
|
8
|
+
if (str.includes(".")) {
|
|
9
|
+
return str.split(".").map(toSnakeCase).join(".");
|
|
10
|
+
}
|
|
11
|
+
return str.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Converts snake_case to camelCase.
|
|
15
|
+
*/
|
|
16
|
+
export function toCamelCase(str) {
|
|
17
|
+
if (!str)
|
|
18
|
+
return str;
|
|
19
|
+
return str.replace(/_([a-z0-9])/g, (_, letter) => letter.toUpperCase());
|
|
20
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@veap/database",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Eloquent-like Active Record ORM powered by Knex with AsyncLocalStorage transactions",
|
|
5
|
+
"bugs": "https://github.com/veapjs/veap-database/issues",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/veapjs/veap-database.git"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./package.json": "./package.json"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"clean": "rimraf dist",
|
|
26
|
+
"build": "rimraf dist && tsc",
|
|
27
|
+
"dev": "tsc --watch",
|
|
28
|
+
"test": "vitest run"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@veap/kernel": "^0.1.0",
|
|
32
|
+
"better-sqlite3": "^13.0.3",
|
|
33
|
+
"knex": "^3.3.0"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"pg": "^8.16.3"
|
|
37
|
+
},
|
|
38
|
+
"peerDependenciesMeta": {
|
|
39
|
+
"pg": {
|
|
40
|
+
"optional": true
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/better-sqlite3": "^9.6.0",
|
|
45
|
+
"@types/node": "^22.20.2",
|
|
46
|
+
"@types/pg": "^8.16.0",
|
|
47
|
+
"pg": "^8.16.3",
|
|
48
|
+
"rimraf": "^6.1.3",
|
|
49
|
+
"typescript": "6.0.3",
|
|
50
|
+
"vitest": "^5.0.0"
|
|
51
|
+
}
|
|
52
|
+
}
|