@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,93 @@
1
+ export class ColumnDefinition {
2
+ private _nullable = false
3
+ private _default: any = undefined
4
+ private _hasDefault = false
5
+ private _unique = false
6
+ private _index = false
7
+ private _unsigned = false
8
+ private _primary = false
9
+ private _references: { table: string; column: string; onDelete?: string; onUpdate?: string } | null = null
10
+ private _comment: string | null = null
11
+ private _after: string | null = null
12
+
13
+ constructor(
14
+ public readonly name: string,
15
+ public readonly type: string,
16
+ public readonly length?: number,
17
+ public readonly precision?: number,
18
+ public readonly scale?: number,
19
+ ) {}
20
+
21
+ nullable(): this {
22
+ this._nullable = true
23
+ return this
24
+ }
25
+
26
+ default(value: any): this {
27
+ this._default = value
28
+ this._hasDefault = true
29
+ return this
30
+ }
31
+
32
+ unique(): this {
33
+ this._unique = true
34
+ return this
35
+ }
36
+
37
+ index(): this {
38
+ this._index = true
39
+ return this
40
+ }
41
+
42
+ unsigned(): this {
43
+ this._unsigned = true
44
+ return this
45
+ }
46
+
47
+ primary(): this {
48
+ this._primary = true
49
+ return this
50
+ }
51
+
52
+ references(column: string): this & { on(table: string): this } {
53
+ this._references = { table: '', column }
54
+ const self = this as any
55
+ self.on = (table: string) => {
56
+ this._references!.table = table
57
+ return self
58
+ }
59
+ return self
60
+ }
61
+
62
+ onDelete(action: 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION'): this {
63
+ if (this._references) this._references.onDelete = action
64
+ return this
65
+ }
66
+
67
+ onUpdate(action: 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION'): this {
68
+ if (this._references) this._references.onUpdate = action
69
+ return this
70
+ }
71
+
72
+ comment(text: string): this {
73
+ this._comment = text
74
+ return this
75
+ }
76
+
77
+ after(column: string): this {
78
+ this._after = column
79
+ return this
80
+ }
81
+
82
+ // Getters for the compiler
83
+ isNullable() { return this._nullable }
84
+ hasDefault() { return this._hasDefault }
85
+ getDefault() { return this._default }
86
+ isUnique() { return this._unique }
87
+ hasIndex() { return this._index }
88
+ isUnsigned() { return this._unsigned }
89
+ isPrimary() { return this._primary }
90
+ getReferences() { return this._references }
91
+ getComment() { return this._comment }
92
+ getAfter() { return this._after }
93
+ }
@@ -0,0 +1,376 @@
1
+ import type { DatabaseConnection } from '../contracts/Connection.ts'
2
+ import { Blueprint } from './Blueprint.ts'
3
+ import type { ColumnDefinition } from './ColumnDefinition.ts'
4
+ import type { ForeignKeyDefinition } from './Blueprint.ts'
5
+
6
+ export interface SchemaBuilder {
7
+ /** Create a new table */
8
+ create(table: string, callback: (blueprint: Blueprint) => void): Promise<void>
9
+ /** Modify an existing table */
10
+ table(table: string, callback: (blueprint: Blueprint) => void): Promise<void>
11
+ /** Drop a table if it exists */
12
+ dropIfExists(table: string): Promise<void>
13
+ /** Drop a table */
14
+ drop(table: string): Promise<void>
15
+ /** Check if a table exists */
16
+ hasTable(table: string): Promise<boolean>
17
+ /** Check if a column exists */
18
+ hasColumn(table: string, column: string): Promise<boolean>
19
+ /** Rename a table */
20
+ rename(from: string, to: string): Promise<void>
21
+ /** Disable FK checks */
22
+ disableForeignKeyConstraints(): Promise<void>
23
+ /** Enable FK checks */
24
+ enableForeignKeyConstraints(): Promise<void>
25
+ }
26
+
27
+ export class SchemaBuilderImpl implements SchemaBuilder {
28
+ constructor(private readonly connection: DatabaseConnection) {}
29
+
30
+ async create(table: string, callback: (blueprint: Blueprint) => void): Promise<void> {
31
+ const bp = new Blueprint()
32
+ callback(bp)
33
+ const sql = this.compileCreate(table, bp)
34
+ for (const s of sql) {
35
+ await this.connection.statement(s)
36
+ }
37
+ }
38
+
39
+ async table(table: string, callback: (blueprint: Blueprint) => void): Promise<void> {
40
+ const bp = new Blueprint()
41
+ callback(bp)
42
+ const sql = this.compileAlter(table, bp)
43
+ for (const s of sql) {
44
+ await this.connection.statement(s)
45
+ }
46
+ }
47
+
48
+ async dropIfExists(table: string): Promise<void> {
49
+ await this.connection.statement(this.compileDropIfExists(table))
50
+ }
51
+
52
+ async drop(table: string): Promise<void> {
53
+ await this.connection.statement(`DROP TABLE ${this.quoteTable(table)}`)
54
+ }
55
+
56
+ async hasTable(table: string): Promise<boolean> {
57
+ const driver = this.connection.getDriverName()
58
+ let sql: string
59
+ let bindings: any[]
60
+
61
+ if (driver === 'sqlite') {
62
+ sql = `SELECT name FROM sqlite_master WHERE type='table' AND name=?`
63
+ bindings = [table]
64
+ } else if (driver === 'postgres') {
65
+ sql = `SELECT table_name FROM information_schema.tables WHERE table_schema='public' AND table_name=$1`
66
+ bindings = [table]
67
+ } else if (driver === 'mssql') {
68
+ sql = `SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE='BASE TABLE' AND TABLE_NAME=@p1`
69
+ bindings = [table]
70
+ } else {
71
+ sql = `SELECT table_name FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name=?`
72
+ bindings = [table]
73
+ }
74
+
75
+ const rows = await this.connection.select(sql, bindings)
76
+ return rows.length > 0
77
+ }
78
+
79
+ async hasColumn(table: string, column: string): Promise<boolean> {
80
+ const driver = this.connection.getDriverName()
81
+ let sql: string
82
+ let bindings: any[]
83
+
84
+ if (driver === 'sqlite') {
85
+ sql = `PRAGMA table_info(${this.quoteTable(table)})`
86
+ const rows = await this.connection.select(sql, [])
87
+ return rows.some((r) => r['name'] === column)
88
+ } else if (driver === 'postgres') {
89
+ sql = `SELECT column_name FROM information_schema.columns WHERE table_name=$1 AND column_name=$2`
90
+ bindings = [table, column]
91
+ } else if (driver === 'mssql') {
92
+ sql = `SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME=@p1 AND COLUMN_NAME=@p2`
93
+ bindings = [table, column]
94
+ } else {
95
+ sql = `SELECT column_name FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name=? AND column_name=?`
96
+ bindings = [table, column]
97
+ }
98
+
99
+ const rows = await this.connection.select(sql, bindings)
100
+ return rows.length > 0
101
+ }
102
+
103
+ async rename(from: string, to: string): Promise<void> {
104
+ const driver = this.connection.getDriverName()
105
+ if (driver === 'mssql') {
106
+ await this.connection.statement(`EXEC sp_rename '${from}', '${to}'`)
107
+ } else {
108
+ await this.connection.statement(`ALTER TABLE ${this.quoteTable(from)} RENAME TO ${this.quoteTable(to)}`)
109
+ }
110
+ }
111
+
112
+ async disableForeignKeyConstraints(): Promise<void> {
113
+ const driver = this.connection.getDriverName()
114
+ if (driver === 'sqlite') {
115
+ await this.connection.statement('PRAGMA foreign_keys = OFF')
116
+ } else if (driver === 'mysql') {
117
+ await this.connection.statement('SET FOREIGN_KEY_CHECKS=0')
118
+ } else if (driver === 'mssql') {
119
+ await this.connection.statement("EXEC sp_MSforeachtable 'ALTER TABLE ? NOCHECK CONSTRAINT ALL'")
120
+ } else {
121
+ await this.connection.statement('SET CONSTRAINTS ALL DEFERRED')
122
+ }
123
+ }
124
+
125
+ async enableForeignKeyConstraints(): Promise<void> {
126
+ const driver = this.connection.getDriverName()
127
+ if (driver === 'sqlite') {
128
+ await this.connection.statement('PRAGMA foreign_keys = ON')
129
+ } else if (driver === 'mysql') {
130
+ await this.connection.statement('SET FOREIGN_KEY_CHECKS=1')
131
+ } else if (driver === 'mssql') {
132
+ await this.connection.statement("EXEC sp_MSforeachtable 'ALTER TABLE ? WITH CHECK CHECK CONSTRAINT ALL'")
133
+ } else {
134
+ await this.connection.statement('SET CONSTRAINTS ALL IMMEDIATE')
135
+ }
136
+ }
137
+
138
+ // ── SQL Compilation ────────────────────────────────────────────────────────
139
+
140
+ private compileCreate(table: string, bp: Blueprint): string[] {
141
+ const driver = this.connection.getDriverName()
142
+ const columnDefs = bp.columns.map((c) => this.compileColumnDef(c, driver))
143
+
144
+ // Inline primary key from PK index
145
+ const pkIndex = bp.indexes.find((i) => i.type === 'primary')
146
+ if (pkIndex) {
147
+ columnDefs.push(`PRIMARY KEY (${pkIndex.columns.map((c) => this.quoteCol(c)).join(', ')})`)
148
+ }
149
+
150
+ // Foreign keys (not for SQLite inline — use separate PRAGMA)
151
+ if (driver !== 'sqlite') {
152
+ for (const fk of bp.foreignKeys) {
153
+ columnDefs.push(this.compileForeignKey(fk))
154
+ }
155
+ }
156
+
157
+ const sql = [`CREATE TABLE ${this.quoteTable(table)} (\n ${columnDefs.join(',\n ')}\n)`]
158
+
159
+ // Column-level unique constraints → separate UNIQUE INDEX statements
160
+ for (const col of bp.columns) {
161
+ if (col.isUnique()) {
162
+ sql.push(this.compileIndex(table, { type: 'unique', columns: [col.name] }))
163
+ }
164
+ if (col.hasIndex()) {
165
+ sql.push(this.compileIndex(table, { type: 'index', columns: [col.name] }))
166
+ }
167
+ }
168
+
169
+ // Blueprint-level indexes
170
+ for (const idx of bp.indexes.filter((i) => i.type !== 'primary')) {
171
+ sql.push(this.compileIndex(table, idx))
172
+ }
173
+
174
+ return sql
175
+ }
176
+
177
+ private compileAlter(table: string, bp: Blueprint): string[] {
178
+ const driver = this.connection.getDriverName()
179
+ const statements: string[] = []
180
+
181
+ for (const col of bp.columns) {
182
+ // MSSQL uses ADD without COLUMN keyword
183
+ const addKw = driver === 'mssql' ? 'ADD' : 'ADD COLUMN'
184
+ statements.push(
185
+ `ALTER TABLE ${this.quoteTable(table)} ${addKw} ${this.compileColumnDef(col, driver)}`,
186
+ )
187
+ }
188
+
189
+ for (const col of bp.droppedColumns) {
190
+ statements.push(`ALTER TABLE ${this.quoteTable(table)} DROP COLUMN ${this.quoteCol(col)}`)
191
+ }
192
+
193
+ for (const idx of bp.indexes) {
194
+ statements.push(this.compileIndex(table, idx))
195
+ }
196
+
197
+ return statements
198
+ }
199
+
200
+ private compileDropIfExists(table: string): string {
201
+ const cascade = this.connection.getDriverName() === 'postgres' ? ' CASCADE' : ''
202
+ return `DROP TABLE IF EXISTS ${this.quoteTable(table)}${cascade}`
203
+ }
204
+
205
+ private compileColumnDef(col: ColumnDefinition, driver: string): string {
206
+ const isAutoIncrement = col.type === 'bigIncrements' || col.type === 'increments'
207
+ let typeSql = this.mapType(col.type, driver, col.length, col.precision, col.scale)
208
+
209
+ if (col.isUnsigned() && driver !== 'sqlite' && driver !== 'mssql') typeSql += ' UNSIGNED'
210
+
211
+ let def = `${this.quoteCol(col.name)} ${typeSql}`
212
+
213
+ if (isAutoIncrement || col.isPrimary()) {
214
+ if (driver === 'sqlite') {
215
+ def += ' PRIMARY KEY AUTOINCREMENT'
216
+ } else if (driver === 'postgres') {
217
+ // SERIAL/BIGSERIAL already implies sequence; just mark PRIMARY KEY
218
+ def += ' PRIMARY KEY'
219
+ } else if (driver === 'mssql') {
220
+ def += ' IDENTITY(1,1) PRIMARY KEY'
221
+ } else {
222
+ def += ' PRIMARY KEY AUTO_INCREMENT'
223
+ }
224
+ }
225
+
226
+ if (!col.isNullable() && !isAutoIncrement && !col.isPrimary()) def += ' NOT NULL'
227
+ if (col.isNullable()) def += ' NULL'
228
+
229
+ // Enum CHECK constraint for Postgres / MSSQL
230
+ if (col.type.startsWith('enum:') && (driver === 'postgres' || driver === 'mssql')) {
231
+ const values = col.type.slice(5).split(',').map((v) => `'${v}'`).join(', ')
232
+ def += ` CHECK (${this.quoteCol(col.name)} IN (${values}))`
233
+ }
234
+
235
+ if (col.hasDefault()) {
236
+ const dv = col.getDefault()
237
+ if (dv === null) {
238
+ def += ' DEFAULT NULL'
239
+ } else if (typeof dv === 'string') {
240
+ def += ` DEFAULT '${dv.replace(/'/g, "''")}'`
241
+ } else if (typeof dv === 'boolean') {
242
+ if (driver === 'postgres') {
243
+ def += ` DEFAULT ${dv ? 'TRUE' : 'FALSE'}`
244
+ } else {
245
+ def += ` DEFAULT ${dv ? 1 : 0}`
246
+ }
247
+ } else {
248
+ def += ` DEFAULT ${dv}`
249
+ }
250
+ }
251
+
252
+ return def
253
+ }
254
+
255
+ private compileIndex(table: string, idx: { type: 'index' | 'unique' | 'primary'; columns: string[]; name?: string }): string {
256
+ const cols = idx.columns.map((c) => this.quoteCol(c)).join(', ')
257
+ const idxName = idx.name ?? `${table}_${idx.columns.join('_')}_${idx.type}`
258
+
259
+ if (idx.type === 'unique') {
260
+ return `CREATE UNIQUE INDEX ${this.quoteCol(idxName)} ON ${this.quoteTable(table)} (${cols})`
261
+ }
262
+ return `CREATE INDEX ${this.quoteCol(idxName)} ON ${this.quoteTable(table)} (${cols})`
263
+ }
264
+
265
+ private compileForeignKey(fk: ForeignKeyDefinition): string {
266
+ let sql = `FOREIGN KEY (${this.quoteCol(fk.column)}) REFERENCES ${this.quoteTable(fk.on)} (${this.quoteCol(fk.references)})`
267
+ if (fk.onDelete) sql += ` ON DELETE ${fk.onDelete}`
268
+ if (fk.onUpdate) sql += ` ON UPDATE ${fk.onUpdate}`
269
+ return sql
270
+ }
271
+
272
+ private mapType(type: string, driver: string, length?: number, precision?: number, scale?: number): string {
273
+ const len = length ?? 255
274
+
275
+ switch (type) {
276
+ case 'bigIncrements':
277
+ if (driver === 'postgres') return 'BIGSERIAL'
278
+ if (driver === 'mysql') return 'BIGINT UNSIGNED'
279
+ if (driver === 'mssql') return 'BIGINT'
280
+ return 'INTEGER' // SQLite uses INTEGER for all auto-increment
281
+ case 'increments':
282
+ if (driver === 'postgres') return 'SERIAL'
283
+ if (driver === 'mysql') return 'INT UNSIGNED'
284
+ if (driver === 'mssql') return 'INT'
285
+ return 'INTEGER'
286
+ case 'string':
287
+ if (driver === 'mssql') return `NVARCHAR(${len})`
288
+ return `VARCHAR(${len})`
289
+ case 'text':
290
+ if (driver === 'mssql') return 'NVARCHAR(MAX)'
291
+ return 'TEXT'
292
+ case 'mediumText':
293
+ if (driver === 'mssql') return 'NVARCHAR(MAX)'
294
+ return driver === 'mysql' ? 'MEDIUMTEXT' : 'TEXT'
295
+ case 'longText':
296
+ if (driver === 'mssql') return 'NVARCHAR(MAX)'
297
+ return driver === 'mysql' ? 'LONGTEXT' : 'TEXT'
298
+ case 'integer':
299
+ if (driver === 'mssql') return 'INT'
300
+ return 'INTEGER'
301
+ case 'bigInteger':
302
+ return 'BIGINT'
303
+ case 'tinyInteger':
304
+ if (driver === 'mysql' || driver === 'mssql') return 'TINYINT'
305
+ return 'INTEGER'
306
+ case 'smallInteger':
307
+ return 'SMALLINT'
308
+ case 'unsignedInteger':
309
+ if (driver === 'mysql') return 'INT UNSIGNED'
310
+ if (driver === 'mssql') return 'INT'
311
+ return 'INTEGER'
312
+ case 'unsignedBigInteger':
313
+ if (driver === 'mysql') return 'BIGINT UNSIGNED'
314
+ if (driver === 'mssql') return 'BIGINT'
315
+ return 'BIGINT'
316
+ case 'float':
317
+ if (driver === 'postgres' || driver === 'mssql') return 'REAL'
318
+ return `FLOAT(${precision ?? 8}, ${scale ?? 2})`
319
+ case 'double':
320
+ if (driver === 'postgres') return 'DOUBLE PRECISION'
321
+ if (driver === 'mssql') return 'FLOAT'
322
+ return `DOUBLE(${precision ?? 15}, ${scale ?? 8})`
323
+ case 'decimal':
324
+ return `DECIMAL(${precision ?? 8}, ${scale ?? 2})`
325
+ case 'boolean':
326
+ if (driver === 'postgres') return 'BOOLEAN'
327
+ if (driver === 'mssql') return 'BIT'
328
+ return 'TINYINT(1)'
329
+ case 'date':
330
+ return 'DATE'
331
+ case 'dateTime':
332
+ if (driver === 'postgres') return 'TIMESTAMP'
333
+ if (driver === 'mssql') return 'DATETIME2'
334
+ return 'DATETIME'
335
+ case 'timestamp':
336
+ if (driver === 'mssql') return 'DATETIME2'
337
+ return 'TIMESTAMP'
338
+ case 'json':
339
+ if (driver === 'sqlite') return 'TEXT'
340
+ if (driver === 'mssql') return 'NVARCHAR(MAX)'
341
+ return 'JSON'
342
+ case 'jsonb':
343
+ if (driver === 'postgres') return 'JSONB'
344
+ if (driver === 'mssql') return 'NVARCHAR(MAX)'
345
+ return 'JSON'
346
+ case 'uuid':
347
+ if (driver === 'postgres') return 'UUID'
348
+ if (driver === 'mssql') return 'UNIQUEIDENTIFIER'
349
+ return 'VARCHAR(36)'
350
+ case 'binary':
351
+ if (driver === 'postgres') return 'BYTEA'
352
+ if (driver === 'mssql') return 'VARBINARY(MAX)'
353
+ return 'BLOB'
354
+ default:
355
+ // Handle enum:val1,val2 syntax
356
+ if (type.startsWith('enum:')) {
357
+ const values = type.slice(5).split(',').map((v) => `'${v}'`).join(', ')
358
+ if (driver === 'postgres' || driver === 'sqlite') return 'TEXT'
359
+ if (driver === 'mssql') return 'NVARCHAR(255)'
360
+ return `ENUM(${values})`
361
+ }
362
+ return type.toUpperCase()
363
+ }
364
+ }
365
+
366
+ private quoteTable(name: string): string {
367
+ const driver = this.connection.getDriverName()
368
+ if (driver === 'mysql') return `\`${name}\``
369
+ if (driver === 'mssql') return `[${name}]`
370
+ return `"${name}"`
371
+ }
372
+
373
+ private quoteCol(name: string): string {
374
+ return this.quoteTable(name)
375
+ }
376
+ }
@@ -0,0 +1,28 @@
1
+ import type { DatabaseConnection } from '../contracts/Connection.ts'
2
+
3
+ export abstract class Seeder {
4
+ protected connection!: DatabaseConnection
5
+
6
+ abstract run(): Promise<void>
7
+
8
+ setConnection(connection: DatabaseConnection): this {
9
+ this.connection = connection
10
+ return this
11
+ }
12
+
13
+ protected async call(SeederClass: new () => Seeder): Promise<void> {
14
+ const seeder = new SeederClass()
15
+ seeder.setConnection(this.connection)
16
+ await seeder.run()
17
+ }
18
+
19
+ protected async callMany(classes: (new () => Seeder)[]): Promise<void> {
20
+ for (const cls of classes) {
21
+ await this.call(cls)
22
+ }
23
+ }
24
+
25
+ table(name: string) {
26
+ return this.connection.table(name)
27
+ }
28
+ }