@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,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
|
+
}
|