@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,154 @@
1
+ import type { DatabaseConnection } from '../contracts/Connection.ts'
2
+ import type { SchemaBuilder } from '../schema/SchemaBuilder.ts'
3
+ import { QueryBuilder } from '../query/Builder.ts'
4
+ import { MSSQLGrammar } from './MSSQLGrammar.ts'
5
+ import { SchemaBuilderImpl } from '../schema/SchemaBuilder.ts'
6
+ import { ConnectionError } from '../errors/ConnectionError.ts'
7
+ import { QueryError } from '../errors/QueryError.ts'
8
+
9
+ export interface MSSQLConfig {
10
+ host?: string
11
+ port?: number
12
+ database: string
13
+ user?: string
14
+ password?: string
15
+ encrypt?: boolean
16
+ trustServerCertificate?: boolean
17
+ pool?: { min?: number; max?: number }
18
+ }
19
+
20
+ export class MSSQLConnection implements DatabaseConnection {
21
+ readonly _grammar = new MSSQLGrammar()
22
+ private pool: any = null
23
+ private config: MSSQLConfig
24
+
25
+ constructor(config: MSSQLConfig) {
26
+ this.config = config
27
+ }
28
+
29
+ private async getPool(): Promise<any> {
30
+ if (!this.pool) {
31
+ try {
32
+ const mssql = await import('mssql')
33
+ const sql = mssql.default ?? mssql
34
+ this.pool = await sql.connect({
35
+ server: this.config.host ?? 'localhost',
36
+ port: this.config.port ?? 1433,
37
+ database: this.config.database,
38
+ user: this.config.user,
39
+ password: this.config.password,
40
+ options: {
41
+ encrypt: this.config.encrypt ?? false,
42
+ trustServerCertificate: this.config.trustServerCertificate ?? true,
43
+ },
44
+ pool: {
45
+ min: this.config.pool?.min ?? 2,
46
+ max: this.config.pool?.max ?? 10,
47
+ },
48
+ })
49
+ } catch (e: any) {
50
+ throw new ConnectionError(`MSSQL connection failed: ${e.message}`, 'mssql', e)
51
+ }
52
+ }
53
+ return this.pool
54
+ }
55
+
56
+ private addInputs(request: any, bindings: any[]): any {
57
+ bindings.forEach((val, i) => {
58
+ request.input(`p${i + 1}`, val)
59
+ })
60
+ return request
61
+ }
62
+
63
+ async select(sql: string, bindings: any[] = []): Promise<Record<string, any>[]> {
64
+ const pool = await this.getPool()
65
+ try {
66
+ const request = this.addInputs(pool.request(), bindings)
67
+ const result = await request.query(sql)
68
+ return result.recordset ?? []
69
+ } catch (e: any) {
70
+ throw new QueryError(sql, bindings, e)
71
+ }
72
+ }
73
+
74
+ async statement(sql: string, bindings: any[] = []): Promise<number> {
75
+ const pool = await this.getPool()
76
+ try {
77
+ const request = this.addInputs(pool.request(), bindings)
78
+ const result = await request.query(sql)
79
+ return result.rowsAffected?.[0] ?? 0
80
+ } catch (e: any) {
81
+ throw new QueryError(sql, bindings, e)
82
+ }
83
+ }
84
+
85
+ async insertGetId(sql: string, bindings: any[] = []): Promise<number | bigint> {
86
+ const pool = await this.getPool()
87
+ try {
88
+ const request = this.addInputs(pool.request(), bindings)
89
+ const result = await request.query(sql)
90
+ return result.recordset?.[0]?.id ?? 0
91
+ } catch (e: any) {
92
+ throw new QueryError(sql, bindings, e)
93
+ }
94
+ }
95
+
96
+ async transaction<T>(callback: (connection: DatabaseConnection) => Promise<T>): Promise<T> {
97
+ const pool = await this.getPool()
98
+ const mssql = await import('mssql')
99
+ const sql = mssql.default ?? mssql
100
+ const transaction = new sql.Transaction(pool)
101
+ await transaction.begin()
102
+ try {
103
+ const txConn: DatabaseConnection = {
104
+ select: async (s, b = []) => {
105
+ const req = transaction.request()
106
+ b.forEach((val: any, i: number) => req.input(`p${i + 1}`, val))
107
+ const r = await req.query(s)
108
+ return r.recordset ?? []
109
+ },
110
+ statement: async (s, b = []) => {
111
+ const req = transaction.request()
112
+ b.forEach((val: any, i: number) => req.input(`p${i + 1}`, val))
113
+ const r = await req.query(s)
114
+ return r.rowsAffected?.[0] ?? 0
115
+ },
116
+ insertGetId: async (s, b = []) => {
117
+ const req = transaction.request()
118
+ b.forEach((val: any, i: number) => req.input(`p${i + 1}`, val))
119
+ const r = await req.query(s)
120
+ return r.recordset?.[0]?.id ?? 0
121
+ },
122
+ transaction: (cb) => cb(txConn),
123
+ table: (name) => new QueryBuilder(txConn, name),
124
+ schema: () => new SchemaBuilderImpl(txConn),
125
+ getDriverName: () => 'mssql',
126
+ getTablePrefix: () => '',
127
+ }
128
+ // @ts-ignore — attach grammar
129
+ txConn._grammar = this._grammar
130
+ const result = await callback(txConn)
131
+ await transaction.commit()
132
+ return result
133
+ } catch (e) {
134
+ await transaction.rollback()
135
+ throw e
136
+ }
137
+ }
138
+
139
+ table(name: string): QueryBuilder {
140
+ return new QueryBuilder(this, name)
141
+ }
142
+
143
+ schema(): SchemaBuilder {
144
+ return new SchemaBuilderImpl(this)
145
+ }
146
+
147
+ getDriverName(): string {
148
+ return 'mssql'
149
+ }
150
+
151
+ getTablePrefix(): string {
152
+ return ''
153
+ }
154
+ }
@@ -0,0 +1,106 @@
1
+ import { BaseGrammar } from './BaseGrammar.ts'
2
+ import { Expression } from '../query/Expression.ts'
3
+ import type { QueryState } from '../query/Builder.ts'
4
+
5
+ export class MSSQLGrammar extends BaseGrammar {
6
+ quoteIdentifier(name: string): string {
7
+ if (name.includes('.')) {
8
+ return name.split('.').map((p) => `[${p}]`).join('.')
9
+ }
10
+ return `[${name}]`
11
+ }
12
+
13
+ placeholder(index: number): string {
14
+ return `@p${index}`
15
+ }
16
+
17
+ /**
18
+ * MSSQL uses TOP for limit-only and OFFSET…FETCH for pagination.
19
+ */
20
+ override compileSelect(state: QueryState): { sql: string; bindings: any[] } {
21
+ const bindings: any[] = []
22
+ const parts: string[] = []
23
+
24
+ // Collect raw expression bindings from columns first
25
+ const colBindings: any[] = []
26
+ for (const c of state.columns) {
27
+ if (c instanceof Expression) colBindings.push(...c.bindings)
28
+ }
29
+
30
+ const cols = state.columns.map((c) => {
31
+ if (c instanceof Expression) return c.value
32
+ const s = c as string
33
+ if (s === '*' || s.endsWith('.*')) return s
34
+ return this.quoteIdentifier(s)
35
+ }).join(', ')
36
+
37
+ // TOP n when limit is set but offset is not
38
+ const useTop = state.limitValue !== null && state.offsetValue === null
39
+ parts.push(`SELECT ${state.distinct ? 'DISTINCT ' : ''}${useTop ? `TOP ${state.limitValue} ` : ''}${cols}`)
40
+ parts.push(`FROM ${this.quoteIdentifier(state.table)}`)
41
+
42
+ // JOINs
43
+ for (const j of state.joins) {
44
+ const type = j.type.toUpperCase()
45
+ parts.push(`${type} JOIN ${this.quoteIdentifier(j.table)} ON ${j.first} ${j.operator} ${j.second}`)
46
+ }
47
+
48
+ // WHERE
49
+ if (state.wheres.length) {
50
+ const { sql: whereSql, bindings: wb } = this.compileWheres(state.wheres, colBindings.length + 1)
51
+ parts.push(`WHERE ${whereSql}`)
52
+ bindings.push(...wb)
53
+ }
54
+
55
+ // GROUP BY
56
+ if (state.groups.length) {
57
+ parts.push(`GROUP BY ${state.groups.map((g) => this.quoteIdentifier(g)).join(', ')}`)
58
+ }
59
+
60
+ // HAVING
61
+ if (state.havings.length) {
62
+ const havingStartIdx = colBindings.length + bindings.length + 1
63
+ const { sql: havingSql, bindings: hb } = this.compileWheres(state.havings, havingStartIdx)
64
+ parts.push(`HAVING ${havingSql}`)
65
+ bindings.push(...hb)
66
+ }
67
+
68
+ // ORDER BY
69
+ if (state.orders.length) {
70
+ const orderStr = state.orders.map((o) => {
71
+ const col = o.column instanceof Expression
72
+ ? o.column.value
73
+ : this.quoteIdentifier(o.column as string)
74
+ return `${col} ${o.direction.toUpperCase()}`
75
+ }).join(', ')
76
+ parts.push(`ORDER BY ${orderStr}`)
77
+ }
78
+
79
+ // OFFSET…FETCH pagination (requires ORDER BY)
80
+ if (state.offsetValue !== null) {
81
+ if (!state.orders.length) {
82
+ parts.push('ORDER BY (SELECT NULL)')
83
+ }
84
+ parts.push(`OFFSET ${state.offsetValue} ROWS`)
85
+ if (state.limitValue !== null) {
86
+ parts.push(`FETCH NEXT ${state.limitValue} ROWS ONLY`)
87
+ }
88
+ }
89
+
90
+ return { sql: parts.join(' '), bindings: [...colBindings, ...bindings] }
91
+ }
92
+
93
+ override compileInsertGetId(table: string, data: Record<string, any>): { sql: string; bindings: any[] } {
94
+ const keys = Object.keys(data)
95
+ const cols = keys.map((k) => this.quoteIdentifier(k)).join(', ')
96
+ const placeholders = keys.map((_, i) => this.placeholder(i + 1)).join(', ')
97
+ return {
98
+ sql: `INSERT INTO ${this.quoteIdentifier(table)} (${cols}) OUTPUT INSERTED.[id] VALUES (${placeholders})`,
99
+ bindings: Object.values(data),
100
+ }
101
+ }
102
+
103
+ override compileTruncate(table: string): string {
104
+ return `TRUNCATE TABLE ${this.quoteIdentifier(table)}`
105
+ }
106
+ }
@@ -0,0 +1,298 @@
1
+ import type {
2
+ MongoDatabaseConnection,
3
+ MongoCollectionContract,
4
+ MongoFilter,
5
+ MongoUpdateDoc,
6
+ MongoPipelineStage,
7
+ MongoInsertResult,
8
+ MongoInsertManyResult,
9
+ MongoUpdateResult,
10
+ MongoDeleteResult,
11
+ MongoQueryBuilder,
12
+ } from '../contracts/MongoConnection.ts'
13
+ import { MongoQueryBuilderImpl } from './MongoQueryBuilderImpl.ts'
14
+ import { ConnectionError } from '../errors/ConnectionError.ts'
15
+ import { QueryError } from '../errors/QueryError.ts'
16
+
17
+ export interface MongoConfig {
18
+ uri: string
19
+ database: string
20
+ options?: Record<string, any>
21
+ }
22
+
23
+ class MongoCollectionImpl implements MongoCollectionContract {
24
+ constructor(
25
+ private readonly col: any,
26
+ private readonly name: string,
27
+ ) {}
28
+
29
+ find(filter: MongoFilter = {}): MongoQueryBuilder {
30
+ return new MongoQueryBuilderImpl(
31
+ this.name,
32
+ async (opts) => {
33
+ let cursor = this.col.find(opts.filter ?? {})
34
+ if (opts.projection) cursor = cursor.project(opts.projection)
35
+ if (opts.sort) cursor = cursor.sort(opts.sort)
36
+ if (opts.skip) cursor = cursor.skip(opts.skip)
37
+ if (opts.limit) cursor = cursor.limit(opts.limit)
38
+ return cursor.toArray()
39
+ },
40
+ async (f) => this.col.countDocuments(f),
41
+ )
42
+ }
43
+
44
+ async findOne(filter: MongoFilter = {}): Promise<Record<string, any> | null> {
45
+ return this.col.findOne(filter)
46
+ }
47
+
48
+ async findById(id: any): Promise<Record<string, any> | null> {
49
+ const { ObjectId } = await import('mongodb')
50
+ return this.col.findOne({ _id: typeof id === 'string' ? new ObjectId(id) : id })
51
+ }
52
+
53
+ async insertOne(doc: Record<string, any>): Promise<MongoInsertResult> {
54
+ const result = await this.col.insertOne(doc)
55
+ return { insertedId: result.insertedId, acknowledged: result.acknowledged }
56
+ }
57
+
58
+ async insertMany(docs: Record<string, any>[]): Promise<MongoInsertManyResult> {
59
+ const result = await this.col.insertMany(docs)
60
+ return {
61
+ insertedIds: Object.values(result.insertedIds),
62
+ acknowledged: result.acknowledged,
63
+ insertedCount: result.insertedCount,
64
+ }
65
+ }
66
+
67
+ async updateOne(filter: MongoFilter, update: MongoUpdateDoc): Promise<MongoUpdateResult> {
68
+ const result = await this.col.updateOne(filter, update)
69
+ return { matchedCount: result.matchedCount, modifiedCount: result.modifiedCount, acknowledged: result.acknowledged }
70
+ }
71
+
72
+ async updateMany(filter: MongoFilter, update: MongoUpdateDoc): Promise<MongoUpdateResult> {
73
+ const result = await this.col.updateMany(filter, update)
74
+ return { matchedCount: result.matchedCount, modifiedCount: result.modifiedCount, acknowledged: result.acknowledged }
75
+ }
76
+
77
+ async replaceOne(filter: MongoFilter, replacement: Record<string, any>): Promise<MongoUpdateResult> {
78
+ const result = await this.col.replaceOne(filter, replacement)
79
+ return { matchedCount: result.matchedCount, modifiedCount: result.modifiedCount, acknowledged: result.acknowledged }
80
+ }
81
+
82
+ async upsert(filter: MongoFilter, update: MongoUpdateDoc): Promise<MongoUpdateResult> {
83
+ const result = await this.col.updateOne(filter, update, { upsert: true })
84
+ return { matchedCount: result.matchedCount, modifiedCount: result.modifiedCount, acknowledged: result.acknowledged }
85
+ }
86
+
87
+ async deleteOne(filter: MongoFilter): Promise<MongoDeleteResult> {
88
+ const result = await this.col.deleteOne(filter)
89
+ return { deletedCount: result.deletedCount, acknowledged: result.acknowledged }
90
+ }
91
+
92
+ async deleteMany(filter: MongoFilter): Promise<MongoDeleteResult> {
93
+ const result = await this.col.deleteMany(filter)
94
+ return { deletedCount: result.deletedCount, acknowledged: result.acknowledged }
95
+ }
96
+
97
+ async aggregate(pipeline: MongoPipelineStage[]): Promise<Record<string, any>[]> {
98
+ return this.col.aggregate(pipeline).toArray()
99
+ }
100
+
101
+ async count(filter: MongoFilter = {}): Promise<number> {
102
+ return this.col.countDocuments(filter)
103
+ }
104
+
105
+ async createIndex(spec: Record<string, any>, options?: Record<string, any>): Promise<string> {
106
+ return this.col.createIndex(spec, options)
107
+ }
108
+
109
+ async drop(): Promise<boolean> {
110
+ return this.col.drop()
111
+ }
112
+ }
113
+
114
+ export class MongoConnection implements MongoDatabaseConnection {
115
+ private client: any = null
116
+ private db: any = null
117
+ private config: MongoConfig
118
+
119
+ constructor(config: MongoConfig) {
120
+ this.config = config
121
+ }
122
+
123
+ private async getDb(): Promise<any> {
124
+ if (!this.db) {
125
+ try {
126
+ const { MongoClient } = await import('mongodb')
127
+ this.client = new MongoClient(this.config.uri, this.config.options ?? {})
128
+ await this.client.connect()
129
+ this.db = this.client.db(this.config.database)
130
+ } catch (e: any) {
131
+ throw new ConnectionError(`MongoDB connection failed: ${e.message}`, 'mongodb', e)
132
+ }
133
+ }
134
+ return this.db
135
+ }
136
+
137
+ collection(name: string): MongoCollectionContract {
138
+ // Lazily get collection — actual DB ops will connect
139
+ const self = this
140
+ const col = {
141
+ async getCol() {
142
+ const db = await self.getDb()
143
+ return db.collection(name)
144
+ },
145
+ }
146
+
147
+ // We need to return a proxy that defers the actual collection resolution
148
+ return new LazyMongoCollection(name, async () => {
149
+ const db = await self.getDb()
150
+ return db.collection(name)
151
+ })
152
+ }
153
+
154
+ async command(command: Record<string, any>): Promise<any> {
155
+ const db = await this.getDb()
156
+ return db.command(command)
157
+ }
158
+
159
+ async listCollections(): Promise<string[]> {
160
+ const db = await this.getDb()
161
+ const collections = await db.listCollections().toArray()
162
+ return collections.map((c: any) => c.name)
163
+ }
164
+
165
+ async transaction<T>(callback: (conn: MongoDatabaseConnection) => Promise<T>): Promise<T> {
166
+ const client = await this.getClient()
167
+ const session = client.startSession()
168
+ try {
169
+ let result!: T
170
+ await session.withTransaction(async () => {
171
+ const txConn = new MongoConnection(this.config)
172
+ txConn.client = this.client
173
+ txConn.db = this.db
174
+ result = await callback(txConn)
175
+ })
176
+ return result
177
+ } finally {
178
+ await session.endSession()
179
+ }
180
+ }
181
+
182
+ private async getClient(): Promise<any> {
183
+ await this.getDb()
184
+ return this.client
185
+ }
186
+
187
+ getDriverName(): string {
188
+ return 'mongodb'
189
+ }
190
+
191
+ async disconnect(): Promise<void> {
192
+ await this.client?.close()
193
+ this.client = null
194
+ this.db = null
195
+ }
196
+ }
197
+
198
+ class LazyMongoCollection implements MongoCollectionContract {
199
+ private _col: any = null
200
+
201
+ constructor(
202
+ private readonly name: string,
203
+ private readonly resolver: () => Promise<any>,
204
+ ) {}
205
+
206
+ private async col(): Promise<any> {
207
+ if (!this._col) this._col = await this.resolver()
208
+ return this._col
209
+ }
210
+
211
+ find(filter: MongoFilter = {}): MongoQueryBuilder {
212
+ return new MongoQueryBuilderImpl(
213
+ this.name,
214
+ async (opts) => {
215
+ const c = await this.col()
216
+ let cursor = c.find(opts.filter ?? {})
217
+ if (opts.projection) cursor = cursor.project(opts.projection)
218
+ if (opts.sort) cursor = cursor.sort(opts.sort)
219
+ if (opts.skip) cursor = cursor.skip(opts.skip)
220
+ if (opts.limit) cursor = cursor.limit(opts.limit)
221
+ return cursor.toArray()
222
+ },
223
+ async (f) => {
224
+ const c = await this.col()
225
+ return c.countDocuments(f)
226
+ },
227
+ )
228
+ }
229
+
230
+ async findOne(filter: MongoFilter = {}): Promise<Record<string, any> | null> {
231
+ return (await this.col()).findOne(filter)
232
+ }
233
+
234
+ async findById(id: any): Promise<Record<string, any> | null> {
235
+ const { ObjectId } = await import('mongodb')
236
+ return (await this.col()).findOne({ _id: typeof id === 'string' ? new ObjectId(id) : id })
237
+ }
238
+
239
+ async insertOne(doc: Record<string, any>): Promise<MongoInsertResult> {
240
+ const result = await (await this.col()).insertOne(doc)
241
+ return { insertedId: result.insertedId, acknowledged: result.acknowledged }
242
+ }
243
+
244
+ async insertMany(docs: Record<string, any>[]): Promise<MongoInsertManyResult> {
245
+ const result = await (await this.col()).insertMany(docs)
246
+ return {
247
+ insertedIds: Object.values(result.insertedIds),
248
+ acknowledged: result.acknowledged,
249
+ insertedCount: result.insertedCount,
250
+ }
251
+ }
252
+
253
+ async updateOne(filter: MongoFilter, update: MongoUpdateDoc): Promise<MongoUpdateResult> {
254
+ const result = await (await this.col()).updateOne(filter, update)
255
+ return { matchedCount: result.matchedCount, modifiedCount: result.modifiedCount, acknowledged: result.acknowledged }
256
+ }
257
+
258
+ async updateMany(filter: MongoFilter, update: MongoUpdateDoc): Promise<MongoUpdateResult> {
259
+ const result = await (await this.col()).updateMany(filter, update)
260
+ return { matchedCount: result.matchedCount, modifiedCount: result.modifiedCount, acknowledged: result.acknowledged }
261
+ }
262
+
263
+ async replaceOne(filter: MongoFilter, replacement: Record<string, any>): Promise<MongoUpdateResult> {
264
+ const result = await (await this.col()).replaceOne(filter, replacement)
265
+ return { matchedCount: result.matchedCount, modifiedCount: result.modifiedCount, acknowledged: result.acknowledged }
266
+ }
267
+
268
+ async upsert(filter: MongoFilter, update: MongoUpdateDoc): Promise<MongoUpdateResult> {
269
+ const result = await (await this.col()).updateOne(filter, update, { upsert: true })
270
+ return { matchedCount: result.matchedCount, modifiedCount: result.modifiedCount, acknowledged: result.acknowledged }
271
+ }
272
+
273
+ async deleteOne(filter: MongoFilter): Promise<MongoDeleteResult> {
274
+ const result = await (await this.col()).deleteOne(filter)
275
+ return { deletedCount: result.deletedCount, acknowledged: result.acknowledged }
276
+ }
277
+
278
+ async deleteMany(filter: MongoFilter): Promise<MongoDeleteResult> {
279
+ const result = await (await this.col()).deleteMany(filter)
280
+ return { deletedCount: result.deletedCount, acknowledged: result.acknowledged }
281
+ }
282
+
283
+ async aggregate(pipeline: MongoPipelineStage[]): Promise<Record<string, any>[]> {
284
+ return (await this.col()).aggregate(pipeline).toArray()
285
+ }
286
+
287
+ async count(filter: MongoFilter = {}): Promise<number> {
288
+ return (await this.col()).countDocuments(filter)
289
+ }
290
+
291
+ async createIndex(spec: Record<string, any>, options?: Record<string, any>): Promise<string> {
292
+ return (await this.col()).createIndex(spec, options)
293
+ }
294
+
295
+ async drop(): Promise<boolean> {
296
+ return (await this.col()).drop()
297
+ }
298
+ }
@@ -0,0 +1,77 @@
1
+ import type {
2
+ MongoFilter,
3
+ MongoProjection,
4
+ MongoSortDoc,
5
+ MongoQueryBuilder,
6
+ } from '../contracts/MongoConnection.ts'
7
+ import { ModelNotFoundError } from '../errors/ModelNotFoundError.ts'
8
+
9
+ export class MongoQueryBuilderImpl implements MongoQueryBuilder {
10
+ private _filter: MongoFilter = {}
11
+ private _projection: MongoProjection | undefined
12
+ private _sort: MongoSortDoc | undefined
13
+ private _limit: number | undefined
14
+ private _skip: number | undefined
15
+
16
+ constructor(
17
+ private readonly collectionName: string,
18
+ private readonly executor: (opts: {
19
+ filter: MongoFilter
20
+ projection?: MongoProjection
21
+ sort?: MongoSortDoc
22
+ limit?: number
23
+ skip?: number
24
+ }) => Promise<Record<string, any>[]>,
25
+ private readonly counter: (filter: MongoFilter) => Promise<number>,
26
+ ) {}
27
+
28
+ where(filter: MongoFilter): this {
29
+ this._filter = { ...this._filter, ...filter }
30
+ return this
31
+ }
32
+
33
+ select(projection: MongoProjection): this {
34
+ this._projection = projection
35
+ return this
36
+ }
37
+
38
+ sort(sort: MongoSortDoc): this {
39
+ this._sort = sort
40
+ return this
41
+ }
42
+
43
+ limit(n: number): this {
44
+ this._limit = n
45
+ return this
46
+ }
47
+
48
+ skip(n: number): this {
49
+ this._skip = n
50
+ return this
51
+ }
52
+
53
+ async get(): Promise<Record<string, any>[]> {
54
+ return this.executor({
55
+ filter: this._filter,
56
+ projection: this._projection,
57
+ sort: this._sort,
58
+ limit: this._limit,
59
+ skip: this._skip,
60
+ })
61
+ }
62
+
63
+ async first(): Promise<Record<string, any> | null> {
64
+ const results = await this.limit(1).get()
65
+ return results[0] ?? null
66
+ }
67
+
68
+ async firstOrFail(): Promise<Record<string, any>> {
69
+ const row = await this.first()
70
+ if (!row) throw new ModelNotFoundError(this.collectionName)
71
+ return row
72
+ }
73
+
74
+ async count(): Promise<number> {
75
+ return this.counter(this._filter)
76
+ }
77
+ }