@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.
Files changed (42) hide show
  1. package/README.md +19 -0
  2. package/package.json +77 -0
  3. package/src/DatabaseManager.ts +115 -0
  4. package/src/DatabaseServiceProvider.ts +39 -0
  5. package/src/contracts/Connection.ts +13 -0
  6. package/src/contracts/Grammar.ts +16 -0
  7. package/src/contracts/MongoConnection.ts +122 -0
  8. package/src/contracts/Paginator.ts +10 -0
  9. package/src/drivers/BaseGrammar.ts +220 -0
  10. package/src/drivers/MSSQLConnection.ts +154 -0
  11. package/src/drivers/MSSQLGrammar.ts +106 -0
  12. package/src/drivers/MongoConnection.ts +298 -0
  13. package/src/drivers/MongoQueryBuilderImpl.ts +77 -0
  14. package/src/drivers/MySQLConnection.ts +120 -0
  15. package/src/drivers/MySQLGrammar.ts +19 -0
  16. package/src/drivers/PostgresConnection.ts +125 -0
  17. package/src/drivers/PostgresGrammar.ts +24 -0
  18. package/src/drivers/SQLiteConnection.ts +125 -0
  19. package/src/drivers/SQLiteGrammar.ts +19 -0
  20. package/src/errors/ConnectionError.ts +10 -0
  21. package/src/errors/ModelNotFoundError.ts +14 -0
  22. package/src/errors/QueryError.ts +11 -0
  23. package/src/events/DatabaseEvents.ts +101 -0
  24. package/src/factories/Factory.ts +170 -0
  25. package/src/factories/Faker.ts +382 -0
  26. package/src/helpers/db.ts +37 -0
  27. package/src/index.ts +100 -0
  28. package/src/migrations/Migration.ts +12 -0
  29. package/src/migrations/MigrationRepository.ts +50 -0
  30. package/src/migrations/Migrator.ts +201 -0
  31. package/src/orm/Collection.ts +236 -0
  32. package/src/orm/Document.ts +202 -0
  33. package/src/orm/Model.ts +775 -0
  34. package/src/orm/ModelQueryBuilder.ts +415 -0
  35. package/src/orm/Scope.ts +39 -0
  36. package/src/orm/eagerLoad.ts +300 -0
  37. package/src/query/Builder.ts +456 -0
  38. package/src/query/Expression.ts +18 -0
  39. package/src/schema/Blueprint.ts +196 -0
  40. package/src/schema/ColumnDefinition.ts +93 -0
  41. package/src/schema/SchemaBuilder.ts +376 -0
  42. 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
+ }