@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,120 @@
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 { MySQLGrammar } from './MySQLGrammar.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 MySQLConfig {
10
+ host?: string
11
+ port?: number
12
+ database: string
13
+ user?: string
14
+ password?: string
15
+ pool?: { min?: number; max?: number }
16
+ }
17
+
18
+ export class MySQLConnection implements DatabaseConnection {
19
+ readonly _grammar = new MySQLGrammar()
20
+ private pool: any = null
21
+ private config: MySQLConfig
22
+
23
+ constructor(config: MySQLConfig) {
24
+ this.config = config
25
+ }
26
+
27
+ private async getPool(): Promise<any> {
28
+ if (!this.pool) {
29
+ try {
30
+ const mysql = await import('mysql2/promise')
31
+ this.pool = await mysql.createPool({
32
+ host: this.config.host ?? 'localhost',
33
+ port: this.config.port ?? 3306,
34
+ database: this.config.database,
35
+ user: this.config.user,
36
+ password: this.config.password,
37
+ connectionLimit: this.config.pool?.max ?? 10,
38
+ waitForConnections: true,
39
+ })
40
+ } catch (e: any) {
41
+ throw new ConnectionError(`MySQL connection failed: ${e.message}`, 'mysql', e)
42
+ }
43
+ }
44
+ return this.pool
45
+ }
46
+
47
+ async select(sql: string, bindings: any[] = []): Promise<Record<string, any>[]> {
48
+ const pool = await this.getPool()
49
+ try {
50
+ const [rows] = await pool.query(sql, bindings)
51
+ return rows as Record<string, any>[]
52
+ } catch (e: any) {
53
+ throw new QueryError(sql, bindings, e)
54
+ }
55
+ }
56
+
57
+ async statement(sql: string, bindings: any[] = []): Promise<number> {
58
+ const pool = await this.getPool()
59
+ try {
60
+ const [result] = await pool.query(sql, bindings)
61
+ return (result as any).affectedRows ?? 0
62
+ } catch (e: any) {
63
+ throw new QueryError(sql, bindings, e)
64
+ }
65
+ }
66
+
67
+ async insertGetId(sql: string, bindings: any[] = []): Promise<number | bigint> {
68
+ const pool = await this.getPool()
69
+ try {
70
+ const [result] = await pool.query(sql, bindings)
71
+ return (result as any).insertId ?? 0
72
+ } catch (e: any) {
73
+ throw new QueryError(sql, bindings, e)
74
+ }
75
+ }
76
+
77
+ async transaction<T>(callback: (connection: DatabaseConnection) => Promise<T>): Promise<T> {
78
+ const pool = await this.getPool()
79
+ const conn = await pool.getConnection()
80
+ try {
81
+ await conn.beginTransaction()
82
+ const txConn: DatabaseConnection = {
83
+ select: async (sql, b) => { const [rows] = await conn.query(sql, b); return rows as Record<string, any>[] },
84
+ statement: async (sql, b) => { const [r] = await conn.query(sql, b); return (r as any).affectedRows ?? 0 },
85
+ insertGetId: async (sql, b) => { const [r] = await conn.query(sql, b); return (r as any).insertId ?? 0 },
86
+ transaction: (cb) => cb(txConn),
87
+ table: (name) => new QueryBuilder(txConn, name),
88
+ schema: () => new SchemaBuilderImpl(txConn),
89
+ getDriverName: () => 'mysql',
90
+ getTablePrefix: () => '',
91
+ }
92
+ // @ts-ignore — attach grammar
93
+ txConn._grammar = this._grammar
94
+ const result = await callback(txConn)
95
+ await conn.commit()
96
+ return result
97
+ } catch (e) {
98
+ await conn.rollback()
99
+ throw e
100
+ } finally {
101
+ conn.release()
102
+ }
103
+ }
104
+
105
+ table(name: string): QueryBuilder {
106
+ return new QueryBuilder(this, name)
107
+ }
108
+
109
+ schema(): SchemaBuilder {
110
+ return new SchemaBuilderImpl(this)
111
+ }
112
+
113
+ getDriverName(): string {
114
+ return 'mysql'
115
+ }
116
+
117
+ getTablePrefix(): string {
118
+ return ''
119
+ }
120
+ }
@@ -0,0 +1,19 @@
1
+ import { BaseGrammar } from './BaseGrammar.ts'
2
+ import type { QueryState } from '../query/Builder.ts'
3
+
4
+ export class MySQLGrammar extends BaseGrammar {
5
+ quoteIdentifier(name: string): string {
6
+ if (name.includes('.')) {
7
+ return name.split('.').map((p) => `\`${p}\``).join('.')
8
+ }
9
+ return `\`${name}\``
10
+ }
11
+
12
+ placeholder(_index: number): string {
13
+ return '?'
14
+ }
15
+
16
+ override compileTruncate(table: string): string {
17
+ return `TRUNCATE TABLE ${this.quoteIdentifier(table)}`
18
+ }
19
+ }
@@ -0,0 +1,125 @@
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 { PostgresGrammar } from './PostgresGrammar.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 PostgresConfig {
10
+ host?: string
11
+ port?: number
12
+ database: string
13
+ user?: string
14
+ password?: string
15
+ ssl?: boolean
16
+ pool?: { min?: number; max?: number }
17
+ }
18
+
19
+ export class PostgresConnection implements DatabaseConnection {
20
+ readonly _grammar = new PostgresGrammar()
21
+ private client: any = null
22
+ private config: PostgresConfig
23
+
24
+ constructor(config: PostgresConfig) {
25
+ this.config = config
26
+ }
27
+
28
+ private async getClient(): Promise<any> {
29
+ if (!this.client) {
30
+ try {
31
+ // Uses pg (node-postgres) compatible driver
32
+ const { default: pg } = await import('pg')
33
+ const pool = new pg.Pool({
34
+ host: this.config.host ?? 'localhost',
35
+ port: this.config.port ?? 5432,
36
+ database: this.config.database,
37
+ user: this.config.user,
38
+ password: this.config.password,
39
+ ssl: this.config.ssl ? { rejectUnauthorized: false } : false,
40
+ min: this.config.pool?.min ?? 2,
41
+ max: this.config.pool?.max ?? 10,
42
+ })
43
+ this.client = pool
44
+ } catch (e: any) {
45
+ throw new ConnectionError(`Postgres connection failed: ${e.message}`, 'postgres', e)
46
+ }
47
+ }
48
+ return this.client
49
+ }
50
+
51
+ async select(sql: string, bindings: any[] = []): Promise<Record<string, any>[]> {
52
+ const pool = await this.getClient()
53
+ try {
54
+ const result = await pool.query(sql, bindings)
55
+ return result.rows
56
+ } catch (e: any) {
57
+ throw new QueryError(sql, bindings, e)
58
+ }
59
+ }
60
+
61
+ async statement(sql: string, bindings: any[] = []): Promise<number> {
62
+ const pool = await this.getClient()
63
+ try {
64
+ const result = await pool.query(sql, bindings)
65
+ return result.rowCount ?? 0
66
+ } catch (e: any) {
67
+ throw new QueryError(sql, bindings, e)
68
+ }
69
+ }
70
+
71
+ async insertGetId(sql: string, bindings: any[] = []): Promise<number | bigint> {
72
+ const pool = await this.getClient()
73
+ try {
74
+ const result = await pool.query(sql, bindings)
75
+ return result.rows[0]?.id ?? 0
76
+ } catch (e: any) {
77
+ throw new QueryError(sql, bindings, e)
78
+ }
79
+ }
80
+
81
+ async transaction<T>(callback: (connection: DatabaseConnection) => Promise<T>): Promise<T> {
82
+ const pool = await this.getClient()
83
+ const client = await pool.connect()
84
+ try {
85
+ await client.query('BEGIN')
86
+ // Create a transactional connection wrapper
87
+ const txConn: DatabaseConnection = {
88
+ select: async (sql, b) => { const r = await client.query(sql, b); return r.rows },
89
+ statement: async (sql, b) => { const r = await client.query(sql, b); return r.rowCount ?? 0 },
90
+ insertGetId: async (sql, b) => { const r = await client.query(sql, b); return r.rows[0]?.id ?? 0 },
91
+ transaction: (cb) => cb(txConn),
92
+ table: (name) => new QueryBuilder(txConn, name),
93
+ schema: () => new SchemaBuilderImpl(txConn),
94
+ getDriverName: () => 'postgres',
95
+ getTablePrefix: () => '',
96
+ }
97
+ // @ts-ignore — attach grammar for the builder
98
+ txConn._grammar = this._grammar
99
+ const result = await callback(txConn)
100
+ await client.query('COMMIT')
101
+ return result
102
+ } catch (e) {
103
+ await client.query('ROLLBACK')
104
+ throw e
105
+ } finally {
106
+ client.release()
107
+ }
108
+ }
109
+
110
+ table(name: string): QueryBuilder {
111
+ return new QueryBuilder(this, name)
112
+ }
113
+
114
+ schema(): SchemaBuilder {
115
+ return new SchemaBuilderImpl(this)
116
+ }
117
+
118
+ getDriverName(): string {
119
+ return 'postgres'
120
+ }
121
+
122
+ getTablePrefix(): string {
123
+ return ''
124
+ }
125
+ }
@@ -0,0 +1,24 @@
1
+ import { BaseGrammar } from './BaseGrammar.ts'
2
+ import type { QueryState } from '../query/Builder.ts'
3
+
4
+ export class PostgresGrammar extends BaseGrammar {
5
+ quoteIdentifier(name: string): string {
6
+ if (name.includes('.')) {
7
+ return name.split('.').map((p) => `"${p}"`).join('.')
8
+ }
9
+ return `"${name}"`
10
+ }
11
+
12
+ placeholder(index: number): string {
13
+ return `$${index}`
14
+ }
15
+
16
+ override compileInsertGetId(table: string, data: Record<string, any>): { sql: string; bindings: any[] } {
17
+ const { sql, bindings } = this.compileInsert(table, data)
18
+ return { sql: `${sql} RETURNING id`, bindings }
19
+ }
20
+
21
+ override compileTruncate(table: string): string {
22
+ return `TRUNCATE TABLE ${this.quoteIdentifier(table)} RESTART IDENTITY CASCADE`
23
+ }
24
+ }
@@ -0,0 +1,125 @@
1
+ import type { DatabaseConnection } from '../contracts/Connection.ts'
2
+ import type { SchemaBuilder } from '../schema/SchemaBuilder.ts'
3
+ import type { EventDispatcher } from '@mantiq/core'
4
+ import { QueryBuilder } from '../query/Builder.ts'
5
+ import { SQLiteGrammar } from './SQLiteGrammar.ts'
6
+ import { SchemaBuilderImpl } from '../schema/SchemaBuilder.ts'
7
+ import { ConnectionError } from '../errors/ConnectionError.ts'
8
+ import { QueryError } from '../errors/QueryError.ts'
9
+ import { QueryExecuted, TransactionBeginning, TransactionCommitted, TransactionRolledBack } from '../events/DatabaseEvents.ts'
10
+
11
+ export interface SQLiteConfig {
12
+ database: string // ':memory:' or file path
13
+ }
14
+
15
+ export class SQLiteConnection implements DatabaseConnection {
16
+ readonly _grammar = new SQLiteGrammar()
17
+ private db: import('bun:sqlite').Database | null = null
18
+ private config: SQLiteConfig
19
+
20
+ /** Optional event dispatcher. Set by @mantiq/events when installed. */
21
+ static _dispatcher: EventDispatcher | null = null
22
+
23
+ constructor(config: SQLiteConfig) {
24
+ this.config = config
25
+ }
26
+
27
+ private getDb(): import('bun:sqlite').Database {
28
+ if (!this.db) {
29
+ try {
30
+ const { Database } = require('bun:sqlite') as typeof import('bun:sqlite')
31
+ this.db = new Database(this.config.database, { create: true })
32
+ this.db.run('PRAGMA journal_mode = WAL')
33
+ this.db.run('PRAGMA foreign_keys = ON')
34
+ } catch (e: any) {
35
+ throw new ConnectionError(`SQLite connection failed: ${e.message}`, 'sqlite', e)
36
+ }
37
+ }
38
+ return this.db
39
+ }
40
+
41
+ /** bun:sqlite only accepts string | number | bigint | boolean | Uint8Array | null */
42
+ private sanitizeBindings(bindings: any[]): any[] {
43
+ return bindings.map((v) => {
44
+ if (v instanceof Date) return v.toISOString()
45
+ return v
46
+ })
47
+ }
48
+
49
+ async select(sql: string, bindings: any[] = []): Promise<Record<string, any>[]> {
50
+ try {
51
+ const start = performance.now()
52
+ const stmt = this.getDb().prepare(sql)
53
+ const result = stmt.all(...this.sanitizeBindings(bindings)) as Record<string, any>[]
54
+ await this.fireQueryEvent(sql, bindings, performance.now() - start)
55
+ return result
56
+ } catch (e: any) {
57
+ throw new QueryError(sql, bindings, e)
58
+ }
59
+ }
60
+
61
+ async statement(sql: string, bindings: any[] = []): Promise<number> {
62
+ try {
63
+ const start = performance.now()
64
+ const stmt = this.getDb().prepare(sql)
65
+ const result = stmt.run(...this.sanitizeBindings(bindings))
66
+ await this.fireQueryEvent(sql, bindings, performance.now() - start)
67
+ return result.changes
68
+ } catch (e: any) {
69
+ throw new QueryError(sql, bindings, e)
70
+ }
71
+ }
72
+
73
+ async insertGetId(sql: string, bindings: any[] = []): Promise<number | bigint> {
74
+ try {
75
+ const start = performance.now()
76
+ const stmt = this.getDb().prepare(sql)
77
+ const result = stmt.run(...this.sanitizeBindings(bindings))
78
+ await this.fireQueryEvent(sql, bindings, performance.now() - start)
79
+ return result.lastInsertRowid
80
+ } catch (e: any) {
81
+ throw new QueryError(sql, bindings, e)
82
+ }
83
+ }
84
+
85
+ async transaction<T>(callback: (connection: DatabaseConnection) => Promise<T>): Promise<T> {
86
+ const db = this.getDb()
87
+ db.run('BEGIN')
88
+ await SQLiteConnection._dispatcher?.emit(new TransactionBeginning('sqlite'))
89
+ try {
90
+ const result = await callback(this)
91
+ db.run('COMMIT')
92
+ await SQLiteConnection._dispatcher?.emit(new TransactionCommitted('sqlite'))
93
+ return result
94
+ } catch (e) {
95
+ db.run('ROLLBACK')
96
+ await SQLiteConnection._dispatcher?.emit(new TransactionRolledBack('sqlite'))
97
+ throw e
98
+ }
99
+ }
100
+
101
+ private async fireQueryEvent(sql: string, bindings: any[], time: number): Promise<void> {
102
+ await SQLiteConnection._dispatcher?.emit(new QueryExecuted(sql, bindings, time, 'sqlite'))
103
+ }
104
+
105
+ table(name: string): QueryBuilder {
106
+ return new QueryBuilder(this, name)
107
+ }
108
+
109
+ schema(): SchemaBuilder {
110
+ return new SchemaBuilderImpl(this)
111
+ }
112
+
113
+ getDriverName(): string {
114
+ return 'sqlite'
115
+ }
116
+
117
+ getTablePrefix(): string {
118
+ return ''
119
+ }
120
+
121
+ close(): void {
122
+ this.db?.close()
123
+ this.db = null
124
+ }
125
+ }
@@ -0,0 +1,19 @@
1
+ import { BaseGrammar } from './BaseGrammar.ts'
2
+
3
+ export class SQLiteGrammar extends BaseGrammar {
4
+ quoteIdentifier(name: string): string {
5
+ // Handle table.column notation
6
+ if (name.includes('.')) {
7
+ return name.split('.').map((p) => `"${p}"`).join('.')
8
+ }
9
+ return `"${name}"`
10
+ }
11
+
12
+ placeholder(_index: number): string {
13
+ return '?'
14
+ }
15
+
16
+ override compileTruncate(table: string): string {
17
+ return `DELETE FROM ${this.quoteIdentifier(table)}`
18
+ }
19
+ }
@@ -0,0 +1,10 @@
1
+ import { MantiqError } from '@mantiq/core'
2
+
3
+ export class ConnectionError extends MantiqError {
4
+ constructor(
5
+ public readonly driver: string,
6
+ originalError: Error,
7
+ ) {
8
+ super(`Failed to connect to ${driver} database: ${originalError.message}`)
9
+ }
10
+ }
@@ -0,0 +1,14 @@
1
+ import { MantiqError } from '@mantiq/core'
2
+
3
+ export class ModelNotFoundError extends MantiqError {
4
+ constructor(
5
+ public readonly modelName: string,
6
+ public readonly id?: any,
7
+ ) {
8
+ super(
9
+ id !== undefined
10
+ ? `No ${modelName} found with ID ${id}.`
11
+ : `No ${modelName} matching the given conditions.`,
12
+ )
13
+ }
14
+ }
@@ -0,0 +1,11 @@
1
+ import { MantiqError } from '@mantiq/core'
2
+
3
+ export class QueryError extends MantiqError {
4
+ constructor(
5
+ public readonly sql: string,
6
+ public readonly bindings: any[],
7
+ public readonly originalError: Error,
8
+ ) {
9
+ super(`Database query failed: ${originalError.message}`, { sql, bindings })
10
+ }
11
+ }
@@ -0,0 +1,101 @@
1
+ import { Event } from '@mantiq/core'
2
+
3
+ /**
4
+ * Fired after a database query is executed.
5
+ * Useful for query logging, debugging, and performance monitoring.
6
+ */
7
+ export class QueryExecuted extends Event {
8
+ constructor(
9
+ /** The SQL query string. */
10
+ public readonly sql: string,
11
+ /** The query bindings. */
12
+ public readonly bindings: any[],
13
+ /** Time in milliseconds the query took to execute. */
14
+ public readonly time: number,
15
+ /** The connection name (e.g. 'sqlite'). */
16
+ public readonly connectionName: string,
17
+ ) {
18
+ super()
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Fired when a database transaction begins.
24
+ */
25
+ export class TransactionBeginning extends Event {
26
+ constructor(public readonly connectionName: string) {
27
+ super()
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Fired when a database transaction is committed.
33
+ */
34
+ export class TransactionCommitted extends Event {
35
+ constructor(public readonly connectionName: string) {
36
+ super()
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Fired when a database transaction is rolled back.
42
+ */
43
+ export class TransactionRolledBack extends Event {
44
+ constructor(public readonly connectionName: string) {
45
+ super()
46
+ }
47
+ }
48
+
49
+ // ── Migration Events ─────────────────────────────────────────────────────────
50
+
51
+ /**
52
+ * Fired before a single migration is executed (up or down).
53
+ */
54
+ export class MigrationStarted extends Event {
55
+ constructor(
56
+ /** The migration name (e.g. '2024_01_01_create_users_table'). */
57
+ public readonly migration: string,
58
+ /** 'up' for running, 'down' for rolling back. */
59
+ public readonly method: 'up' | 'down',
60
+ ) {
61
+ super()
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Fired after a single migration has been executed (up or down).
67
+ */
68
+ export class MigrationEnded extends Event {
69
+ constructor(
70
+ /** The migration name. */
71
+ public readonly migration: string,
72
+ /** 'up' for running, 'down' for rolling back. */
73
+ public readonly method: 'up' | 'down',
74
+ ) {
75
+ super()
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Fired before a batch of migrations starts (run/rollback/reset).
81
+ */
82
+ export class MigrationsStarted extends Event {
83
+ constructor(
84
+ /** 'up' for running, 'down' for rolling back. */
85
+ public readonly method: 'up' | 'down',
86
+ ) {
87
+ super()
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Fired after a batch of migrations finishes (run/rollback/reset).
93
+ */
94
+ export class MigrationsEnded extends Event {
95
+ constructor(
96
+ /** 'up' for running, 'down' for rolling back. */
97
+ public readonly method: 'up' | 'down',
98
+ ) {
99
+ super()
100
+ }
101
+ }