@mantiq/database 0.0.1
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/README.md +19 -0
- package/package.json +77 -0
- package/src/DatabaseManager.ts +115 -0
- package/src/DatabaseServiceProvider.ts +39 -0
- package/src/contracts/Connection.ts +13 -0
- package/src/contracts/Grammar.ts +16 -0
- package/src/contracts/MongoConnection.ts +122 -0
- package/src/contracts/Paginator.ts +10 -0
- package/src/drivers/BaseGrammar.ts +220 -0
- package/src/drivers/MSSQLConnection.ts +154 -0
- package/src/drivers/MSSQLGrammar.ts +106 -0
- package/src/drivers/MongoConnection.ts +298 -0
- package/src/drivers/MongoQueryBuilderImpl.ts +77 -0
- package/src/drivers/MySQLConnection.ts +120 -0
- package/src/drivers/MySQLGrammar.ts +19 -0
- package/src/drivers/PostgresConnection.ts +125 -0
- package/src/drivers/PostgresGrammar.ts +24 -0
- package/src/drivers/SQLiteConnection.ts +125 -0
- package/src/drivers/SQLiteGrammar.ts +19 -0
- package/src/errors/ConnectionError.ts +10 -0
- package/src/errors/ModelNotFoundError.ts +14 -0
- package/src/errors/QueryError.ts +11 -0
- package/src/events/DatabaseEvents.ts +101 -0
- package/src/factories/Factory.ts +170 -0
- package/src/factories/Faker.ts +382 -0
- package/src/helpers/db.ts +37 -0
- package/src/index.ts +100 -0
- package/src/migrations/Migration.ts +12 -0
- package/src/migrations/MigrationRepository.ts +50 -0
- package/src/migrations/Migrator.ts +201 -0
- package/src/orm/Collection.ts +236 -0
- package/src/orm/Document.ts +202 -0
- package/src/orm/Model.ts +775 -0
- package/src/orm/ModelQueryBuilder.ts +415 -0
- package/src/orm/Scope.ts +39 -0
- package/src/orm/eagerLoad.ts +300 -0
- package/src/query/Builder.ts +456 -0
- package/src/query/Expression.ts +18 -0
- package/src/schema/Blueprint.ts +196 -0
- package/src/schema/ColumnDefinition.ts +93 -0
- package/src/schema/SchemaBuilder.ts +376 -0
- package/src/seeders/Seeder.ts +28 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import type { Model, ModelStatic } from './Model.ts'
|
|
2
|
+
import type { ModelQueryBuilder } from './ModelQueryBuilder.ts'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Eager-load relation configuration.
|
|
6
|
+
* Supports string names or an object with constraint callbacks.
|
|
7
|
+
*/
|
|
8
|
+
export type EagerLoadSpec =
|
|
9
|
+
| string
|
|
10
|
+
| Record<string, (query: ModelQueryBuilder<any>) => void>
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Normalize `with()` arguments into a map of relation name → optional constraint callback.
|
|
14
|
+
*
|
|
15
|
+
* Supports:
|
|
16
|
+
* with('posts', 'profile') → { posts: null, profile: null }
|
|
17
|
+
* with({ posts: q => q.where(...) }) → { posts: <fn> }
|
|
18
|
+
* with('posts.comments') → { posts: null } (nested handled recursively)
|
|
19
|
+
*/
|
|
20
|
+
export function normalizeEagerLoads(
|
|
21
|
+
...specs: EagerLoadSpec[]
|
|
22
|
+
): Map<string, ((query: ModelQueryBuilder<any>) => void) | null> {
|
|
23
|
+
const map = new Map<string, ((query: ModelQueryBuilder<any>) => void) | null>()
|
|
24
|
+
|
|
25
|
+
for (const spec of specs) {
|
|
26
|
+
if (typeof spec === 'string') {
|
|
27
|
+
// Handle dot-notation: 'posts.comments' → eager load 'posts', then nested 'comments'
|
|
28
|
+
const root = spec.split('.')[0]!
|
|
29
|
+
map.set(root, map.get(root) ?? null)
|
|
30
|
+
} else {
|
|
31
|
+
for (const [name, constraint] of Object.entries(spec)) {
|
|
32
|
+
const root = name.split('.')[0]!
|
|
33
|
+
map.set(root, constraint)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return map
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Extract nested relations from dot-notation specs.
|
|
43
|
+
* e.g., ['posts.comments', 'posts.tags'] → { posts: ['comments', 'tags'] }
|
|
44
|
+
*/
|
|
45
|
+
export function extractNestedRelations(specs: string[]): Map<string, string[]> {
|
|
46
|
+
const nested = new Map<string, string[]>()
|
|
47
|
+
|
|
48
|
+
for (const spec of specs) {
|
|
49
|
+
const dotIndex = spec.indexOf('.')
|
|
50
|
+
if (dotIndex === -1) continue
|
|
51
|
+
const root = spec.substring(0, dotIndex)
|
|
52
|
+
const rest = spec.substring(dotIndex + 1)
|
|
53
|
+
if (!nested.has(root)) nested.set(root, [])
|
|
54
|
+
nested.get(root)!.push(rest)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return nested
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Eager-load relations on a set of already-fetched model instances.
|
|
62
|
+
* Uses the N+1-safe approach: one query per relation regardless of parent count.
|
|
63
|
+
*
|
|
64
|
+
* @param models The parent models to load relations onto
|
|
65
|
+
* @param relations Array of relation names (may include dot-notation for nesting)
|
|
66
|
+
* @param constraints Optional map of relation name → query constraint callback
|
|
67
|
+
*/
|
|
68
|
+
export async function eagerLoadRelations<T extends Model>(
|
|
69
|
+
models: T[],
|
|
70
|
+
relations: string[],
|
|
71
|
+
constraints?: Map<string, ((query: ModelQueryBuilder<any>) => void) | null>,
|
|
72
|
+
): Promise<void> {
|
|
73
|
+
if (models.length === 0) return
|
|
74
|
+
|
|
75
|
+
const nestedMap = extractNestedRelations(relations)
|
|
76
|
+
|
|
77
|
+
// Deduplicate root relations
|
|
78
|
+
const rootRelations = [...new Set(relations.map((r) => r.split('.')[0]!))]
|
|
79
|
+
|
|
80
|
+
for (const relationName of rootRelations) {
|
|
81
|
+
const firstModel = models[0]!
|
|
82
|
+
const ctor = firstModel.constructor as typeof Model
|
|
83
|
+
|
|
84
|
+
// Check if the model has this relation method
|
|
85
|
+
if (typeof (firstModel as any)[relationName] !== 'function') {
|
|
86
|
+
throw new Error(`Relation '${relationName}' is not defined on model '${ctor.table}'.`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Get the relation definition from the first model to understand the type
|
|
90
|
+
const sampleRelation = (firstModel as any)[relationName]()
|
|
91
|
+
const constraint = constraints?.get(relationName) ?? null
|
|
92
|
+
|
|
93
|
+
if (sampleRelation.constructor.name === 'HasOneRelation') {
|
|
94
|
+
await eagerLoadHasOne(models, relationName, sampleRelation, constraint)
|
|
95
|
+
} else if (sampleRelation.constructor.name === 'HasManyRelation') {
|
|
96
|
+
await eagerLoadHasMany(models, relationName, sampleRelation, constraint)
|
|
97
|
+
} else if (sampleRelation.constructor.name === 'BelongsToRelation') {
|
|
98
|
+
await eagerLoadBelongsTo(models, relationName, sampleRelation, constraint)
|
|
99
|
+
} else if (sampleRelation.constructor.name === 'BelongsToManyRelation') {
|
|
100
|
+
await eagerLoadBelongsToMany(models, relationName, sampleRelation, constraint)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Handle nested relations (e.g., 'posts.comments')
|
|
104
|
+
const nestedSpecs = nestedMap.get(relationName)
|
|
105
|
+
if (nestedSpecs?.length) {
|
|
106
|
+
// Collect all loaded related models
|
|
107
|
+
const relatedModels: Model[] = []
|
|
108
|
+
for (const model of models) {
|
|
109
|
+
const loaded = (model as any)._relations[relationName]
|
|
110
|
+
if (Array.isArray(loaded)) {
|
|
111
|
+
relatedModels.push(...loaded)
|
|
112
|
+
} else if (loaded) {
|
|
113
|
+
relatedModels.push(loaded)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (relatedModels.length > 0) {
|
|
117
|
+
await eagerLoadRelations(relatedModels, nestedSpecs)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── HasOne eager loading ───────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
async function eagerLoadHasOne<T extends Model>(
|
|
126
|
+
models: T[],
|
|
127
|
+
relationName: string,
|
|
128
|
+
sampleRelation: any,
|
|
129
|
+
constraint: ((query: ModelQueryBuilder<any>) => void) | null,
|
|
130
|
+
): Promise<void> {
|
|
131
|
+
const related = sampleRelation['related'] as ModelStatic<any>
|
|
132
|
+
const foreignKey = sampleRelation['foreignKey'] as string
|
|
133
|
+
const ctor = models[0]!.constructor as typeof Model
|
|
134
|
+
const localKey = ctor.primaryKey
|
|
135
|
+
|
|
136
|
+
const parentIds = models.map((m) => (m as any)._attributes[localKey]).filter((id) => id != null)
|
|
137
|
+
if (!parentIds.length) return
|
|
138
|
+
|
|
139
|
+
let query = related.query().whereIn(foreignKey, parentIds)
|
|
140
|
+
if (constraint) constraint(query)
|
|
141
|
+
|
|
142
|
+
const results = await query.get()
|
|
143
|
+
const resultMap = new Map<any, any>()
|
|
144
|
+
for (const result of results) {
|
|
145
|
+
resultMap.set((result as any)._attributes[foreignKey], result)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for (const model of models) {
|
|
149
|
+
const parentId = (model as any)._attributes[localKey]
|
|
150
|
+
;(model as any)._relations[relationName] = resultMap.get(parentId) ?? null
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── HasMany eager loading ──────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
async function eagerLoadHasMany<T extends Model>(
|
|
157
|
+
models: T[],
|
|
158
|
+
relationName: string,
|
|
159
|
+
sampleRelation: any,
|
|
160
|
+
constraint: ((query: ModelQueryBuilder<any>) => void) | null,
|
|
161
|
+
): Promise<void> {
|
|
162
|
+
const related = sampleRelation['related'] as ModelStatic<any>
|
|
163
|
+
const foreignKey = sampleRelation['foreignKey'] as string
|
|
164
|
+
const ctor = models[0]!.constructor as typeof Model
|
|
165
|
+
const localKey = ctor.primaryKey
|
|
166
|
+
|
|
167
|
+
const parentIds = models.map((m) => (m as any)._attributes[localKey]).filter((id) => id != null)
|
|
168
|
+
if (!parentIds.length) return
|
|
169
|
+
|
|
170
|
+
let query = related.query().whereIn(foreignKey, parentIds)
|
|
171
|
+
if (constraint) constraint(query)
|
|
172
|
+
|
|
173
|
+
const results = await query.get()
|
|
174
|
+
const resultMap = new Map<any, any[]>()
|
|
175
|
+
for (const result of results) {
|
|
176
|
+
const fkValue = (result as any)._attributes[foreignKey]
|
|
177
|
+
if (!resultMap.has(fkValue)) resultMap.set(fkValue, [])
|
|
178
|
+
resultMap.get(fkValue)!.push(result)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for (const model of models) {
|
|
182
|
+
const parentId = (model as any)._attributes[localKey]
|
|
183
|
+
;(model as any)._relations[relationName] = resultMap.get(parentId) ?? []
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── BelongsTo eager loading ────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
async function eagerLoadBelongsTo<T extends Model>(
|
|
190
|
+
models: T[],
|
|
191
|
+
relationName: string,
|
|
192
|
+
sampleRelation: any,
|
|
193
|
+
constraint: ((query: ModelQueryBuilder<any>) => void) | null,
|
|
194
|
+
): Promise<void> {
|
|
195
|
+
const related = sampleRelation['related'] as ModelStatic<any>
|
|
196
|
+
const ownerKey = sampleRelation['ownerKey'] as string
|
|
197
|
+
|
|
198
|
+
// Need to figure out the foreign key from the model's attributes
|
|
199
|
+
// The BelongsToRelation stores the foreignId, but we need the foreignKey name
|
|
200
|
+
// We derive it from the relation definition
|
|
201
|
+
const relatedCtor = related as typeof Model
|
|
202
|
+
const foreignKey = guessBelongsToForeignKey(models[0]!, relationName, relatedCtor)
|
|
203
|
+
|
|
204
|
+
const foreignIds = models.map((m) => (m as any)._attributes[foreignKey]).filter((id) => id != null)
|
|
205
|
+
if (!foreignIds.length) return
|
|
206
|
+
|
|
207
|
+
const uniqueIds = [...new Set(foreignIds)]
|
|
208
|
+
let query = related.query().whereIn(ownerKey, uniqueIds)
|
|
209
|
+
if (constraint) constraint(query)
|
|
210
|
+
|
|
211
|
+
const results = await query.get()
|
|
212
|
+
const resultMap = new Map<any, any>()
|
|
213
|
+
for (const result of results) {
|
|
214
|
+
resultMap.set((result as any)._attributes[ownerKey], result)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
for (const model of models) {
|
|
218
|
+
const fkValue = (model as any)._attributes[foreignKey]
|
|
219
|
+
;(model as any)._relations[relationName] = resultMap.get(fkValue) ?? null
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function guessBelongsToForeignKey(model: Model, relationName: string, relatedCtor: typeof Model): string {
|
|
224
|
+
// Convention: relation name + '_id' (e.g., 'author' → 'author_id')
|
|
225
|
+
// But we also try the related model's table name in singular + '_id'
|
|
226
|
+
const snaked = relationName
|
|
227
|
+
.replace(/([a-z])([A-Z])/g, '$1_$2')
|
|
228
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
|
|
229
|
+
.toLowerCase()
|
|
230
|
+
const possibleKey = `${snaked}_id`
|
|
231
|
+
if (possibleKey in (model as any)._attributes) return possibleKey
|
|
232
|
+
|
|
233
|
+
// Fallback: related model name in snake_case + '_id'
|
|
234
|
+
const relatedName = relatedCtor.name
|
|
235
|
+
.replace(/([a-z])([A-Z])/g, '$1_$2')
|
|
236
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
|
|
237
|
+
.toLowerCase()
|
|
238
|
+
return `${relatedName}_id`
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── BelongsToMany eager loading ────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
async function eagerLoadBelongsToMany<T extends Model>(
|
|
244
|
+
models: T[],
|
|
245
|
+
relationName: string,
|
|
246
|
+
sampleRelation: any,
|
|
247
|
+
constraint: ((query: ModelQueryBuilder<any>) => void) | null,
|
|
248
|
+
): Promise<void> {
|
|
249
|
+
const related = sampleRelation['related'] as ModelStatic<any>
|
|
250
|
+
const pivotTable = sampleRelation['pivotTable'] as string
|
|
251
|
+
const foreignKey = sampleRelation['foreignKey'] as string
|
|
252
|
+
const relatedKey = sampleRelation['relatedKey'] as string
|
|
253
|
+
const relatedCtor = related as typeof Model
|
|
254
|
+
const ctor = models[0]!.constructor as typeof Model
|
|
255
|
+
const localKey = ctor.primaryKey
|
|
256
|
+
|
|
257
|
+
if (!relatedCtor.connection) return
|
|
258
|
+
|
|
259
|
+
const parentIds = models.map((m) => (m as any)._attributes[localKey]).filter((id) => id != null)
|
|
260
|
+
if (!parentIds.length) return
|
|
261
|
+
|
|
262
|
+
// Step 1: Get all pivot rows for these parent IDs
|
|
263
|
+
const pivotRows = await relatedCtor.connection.table(pivotTable)
|
|
264
|
+
.whereIn(foreignKey, parentIds)
|
|
265
|
+
.get()
|
|
266
|
+
|
|
267
|
+
if (!pivotRows.length) {
|
|
268
|
+
for (const model of models) {
|
|
269
|
+
;(model as any)._relations[relationName] = []
|
|
270
|
+
}
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Step 2: Get all related models
|
|
275
|
+
const relatedIds = [...new Set(pivotRows.map((r) => r[relatedKey]))]
|
|
276
|
+
let query = related.query().whereIn(relatedCtor.primaryKey, relatedIds)
|
|
277
|
+
if (constraint) constraint(query)
|
|
278
|
+
|
|
279
|
+
const results = await query.get()
|
|
280
|
+
const resultMap = new Map<any, any>()
|
|
281
|
+
for (const result of results) {
|
|
282
|
+
resultMap.set((result as any)._attributes[relatedCtor.primaryKey], result)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Step 3: Build parent → related[] mapping via pivot
|
|
286
|
+
const parentRelatedMap = new Map<any, any[]>()
|
|
287
|
+
for (const pivot of pivotRows) {
|
|
288
|
+
const parentId = pivot[foreignKey]
|
|
289
|
+
const relId = pivot[relatedKey]
|
|
290
|
+
const relModel = resultMap.get(relId)
|
|
291
|
+
if (!relModel) continue
|
|
292
|
+
if (!parentRelatedMap.has(parentId)) parentRelatedMap.set(parentId, [])
|
|
293
|
+
parentRelatedMap.get(parentId)!.push(relModel)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
for (const model of models) {
|
|
297
|
+
const parentId = (model as any)._attributes[localKey]
|
|
298
|
+
;(model as any)._relations[relationName] = parentRelatedMap.get(parentId) ?? []
|
|
299
|
+
}
|
|
300
|
+
}
|