@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
package/src/index.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// ── Contracts ────────────────────────────────────────────────────────────────
|
|
2
|
+
export type { DatabaseConnection } from './contracts/Connection.ts'
|
|
3
|
+
export type { Grammar } from './contracts/Grammar.ts'
|
|
4
|
+
export type { PaginationResult } from './contracts/Paginator.ts'
|
|
5
|
+
export type {
|
|
6
|
+
MongoDatabaseConnection,
|
|
7
|
+
MongoCollectionContract,
|
|
8
|
+
MongoQueryBuilder,
|
|
9
|
+
MongoFilter,
|
|
10
|
+
MongoUpdateDoc,
|
|
11
|
+
MongoPipelineStage,
|
|
12
|
+
MongoSortDoc,
|
|
13
|
+
MongoProjection,
|
|
14
|
+
MongoInsertResult,
|
|
15
|
+
MongoInsertManyResult,
|
|
16
|
+
MongoUpdateResult,
|
|
17
|
+
MongoDeleteResult,
|
|
18
|
+
} from './contracts/MongoConnection.ts'
|
|
19
|
+
|
|
20
|
+
// ── Errors ────────────────────────────────────────────────────────────────────
|
|
21
|
+
export { QueryError } from './errors/QueryError.ts'
|
|
22
|
+
export { ModelNotFoundError } from './errors/ModelNotFoundError.ts'
|
|
23
|
+
export { ConnectionError } from './errors/ConnectionError.ts'
|
|
24
|
+
|
|
25
|
+
// ── Query Builder ─────────────────────────────────────────────────────────────
|
|
26
|
+
export { QueryBuilder } from './query/Builder.ts'
|
|
27
|
+
export { Expression, raw } from './query/Expression.ts'
|
|
28
|
+
export type { QueryState, WhereClause, JoinClause, OrderClause, Operator } from './query/Builder.ts'
|
|
29
|
+
|
|
30
|
+
// ── Grammar ───────────────────────────────────────────────────────────────────
|
|
31
|
+
export { BaseGrammar } from './drivers/BaseGrammar.ts'
|
|
32
|
+
export { SQLiteGrammar } from './drivers/SQLiteGrammar.ts'
|
|
33
|
+
export { PostgresGrammar } from './drivers/PostgresGrammar.ts'
|
|
34
|
+
export { MySQLGrammar } from './drivers/MySQLGrammar.ts'
|
|
35
|
+
export { MSSQLGrammar } from './drivers/MSSQLGrammar.ts'
|
|
36
|
+
|
|
37
|
+
// ── SQL Connections ───────────────────────────────────────────────────────────
|
|
38
|
+
export { SQLiteConnection } from './drivers/SQLiteConnection.ts'
|
|
39
|
+
export type { SQLiteConfig } from './drivers/SQLiteConnection.ts'
|
|
40
|
+
export { PostgresConnection } from './drivers/PostgresConnection.ts'
|
|
41
|
+
export type { PostgresConfig } from './drivers/PostgresConnection.ts'
|
|
42
|
+
export { MySQLConnection } from './drivers/MySQLConnection.ts'
|
|
43
|
+
export type { MySQLConfig } from './drivers/MySQLConnection.ts'
|
|
44
|
+
export { MSSQLConnection } from './drivers/MSSQLConnection.ts'
|
|
45
|
+
export type { MSSQLConfig } from './drivers/MSSQLConnection.ts'
|
|
46
|
+
|
|
47
|
+
// ── MongoDB ───────────────────────────────────────────────────────────────────
|
|
48
|
+
export { MongoConnection } from './drivers/MongoConnection.ts'
|
|
49
|
+
export type { MongoConfig } from './drivers/MongoConnection.ts'
|
|
50
|
+
|
|
51
|
+
// ── Schema Builder ────────────────────────────────────────────────────────────
|
|
52
|
+
export type { SchemaBuilder } from './schema/SchemaBuilder.ts'
|
|
53
|
+
export { SchemaBuilderImpl } from './schema/SchemaBuilder.ts'
|
|
54
|
+
export { Blueprint } from './schema/Blueprint.ts'
|
|
55
|
+
export { ColumnDefinition } from './schema/ColumnDefinition.ts'
|
|
56
|
+
export type { IndexDefinition, ForeignKeyDefinition } from './schema/Blueprint.ts'
|
|
57
|
+
|
|
58
|
+
// ── Migrations ────────────────────────────────────────────────────────────────
|
|
59
|
+
export { Migration } from './migrations/Migration.ts'
|
|
60
|
+
export type { MigrationContract } from './migrations/Migration.ts'
|
|
61
|
+
export { Migrator } from './migrations/Migrator.ts'
|
|
62
|
+
export type { MigrationFile } from './migrations/Migrator.ts'
|
|
63
|
+
export { MigrationRepository } from './migrations/MigrationRepository.ts'
|
|
64
|
+
|
|
65
|
+
// ── ORM ───────────────────────────────────────────────────────────────────────
|
|
66
|
+
export { Model } from './orm/Model.ts'
|
|
67
|
+
export type { Scope } from './orm/Scope.ts'
|
|
68
|
+
export { ClosureScope } from './orm/Scope.ts'
|
|
69
|
+
export {
|
|
70
|
+
HasOneRelation,
|
|
71
|
+
HasManyRelation,
|
|
72
|
+
BelongsToRelation,
|
|
73
|
+
BelongsToManyRelation,
|
|
74
|
+
} from './orm/Model.ts'
|
|
75
|
+
export type { ModelStatic } from './orm/Model.ts'
|
|
76
|
+
export { ModelQueryBuilder } from './orm/ModelQueryBuilder.ts'
|
|
77
|
+
export { Collection } from './orm/Collection.ts'
|
|
78
|
+
|
|
79
|
+
// ── MongoDB Document ORM ──────────────────────────────────────────────────────
|
|
80
|
+
export { Document } from './orm/Document.ts'
|
|
81
|
+
|
|
82
|
+
// ── Seeders & Factories ───────────────────────────────────────────────────────
|
|
83
|
+
export { Seeder } from './seeders/Seeder.ts'
|
|
84
|
+
export { Factory } from './factories/Factory.ts'
|
|
85
|
+
export { Faker } from './factories/Faker.ts'
|
|
86
|
+
|
|
87
|
+
// ── Database Manager ──────────────────────────────────────────────────────────
|
|
88
|
+
export { DatabaseManager } from './DatabaseManager.ts'
|
|
89
|
+
export type { DatabaseConfig, ConnectionConfig, SQLConfig } from './DatabaseManager.ts'
|
|
90
|
+
export type { MongoConfig as MongoConnectionConfig } from './DatabaseManager.ts'
|
|
91
|
+
|
|
92
|
+
// ── Bootstrap helpers ─────────────────────────────────────────────────────────
|
|
93
|
+
export { createDatabaseManager, setupModels } from './DatabaseServiceProvider.ts'
|
|
94
|
+
|
|
95
|
+
// ── Events ───────────────────────────────────────────────────────────────────
|
|
96
|
+
export { QueryExecuted, TransactionBeginning, TransactionCommitted, TransactionRolledBack } from './events/DatabaseEvents.ts'
|
|
97
|
+
export { MigrationStarted, MigrationEnded, MigrationsStarted, MigrationsEnded } from './events/DatabaseEvents.ts'
|
|
98
|
+
|
|
99
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
100
|
+
export { db, table, schema, mongo, collection, setManager, getManager } from './helpers/db.ts'
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SchemaBuilder } from '../schema/SchemaBuilder.ts'
|
|
2
|
+
import type { DatabaseConnection } from '../contracts/Connection.ts'
|
|
3
|
+
|
|
4
|
+
export interface MigrationContract {
|
|
5
|
+
up(schema: SchemaBuilder, db: DatabaseConnection): Promise<void>
|
|
6
|
+
down(schema: SchemaBuilder, db: DatabaseConnection): Promise<void>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export abstract class Migration implements MigrationContract {
|
|
10
|
+
abstract up(schema: SchemaBuilder, db: DatabaseConnection): Promise<void>
|
|
11
|
+
abstract down(schema: SchemaBuilder, db: DatabaseConnection): Promise<void>
|
|
12
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { DatabaseConnection } from '../contracts/Connection.ts'
|
|
2
|
+
|
|
3
|
+
export class MigrationRepository {
|
|
4
|
+
private readonly table = 'migrations'
|
|
5
|
+
|
|
6
|
+
constructor(private readonly connection: DatabaseConnection) {}
|
|
7
|
+
|
|
8
|
+
async createTable(): Promise<void> {
|
|
9
|
+
const exists = await this.connection.schema().hasTable(this.table)
|
|
10
|
+
if (!exists) {
|
|
11
|
+
await this.connection.schema().create(this.table, (t) => {
|
|
12
|
+
t.id()
|
|
13
|
+
t.string('migration', 255)
|
|
14
|
+
t.integer('batch')
|
|
15
|
+
t.timestamp('run_at').nullable()
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async getRan(): Promise<string[]> {
|
|
21
|
+
const rows = await this.connection.table(this.table).pluck('migration')
|
|
22
|
+
return rows as string[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async getLastBatch(): Promise<number> {
|
|
26
|
+
const result = await this.connection.table(this.table).max('batch')
|
|
27
|
+
return Number(result ?? 0)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async log(migration: string, batch: number): Promise<void> {
|
|
31
|
+
await this.connection.table(this.table).insert({
|
|
32
|
+
migration,
|
|
33
|
+
batch,
|
|
34
|
+
run_at: new Date(),
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async delete(migration: string): Promise<void> {
|
|
39
|
+
await this.connection.table(this.table).where('migration', migration).delete()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async getLastBatchMigrations(): Promise<string[]> {
|
|
43
|
+
const batch = await this.getLastBatch()
|
|
44
|
+
if (batch === 0) return []
|
|
45
|
+
return this.connection.table(this.table)
|
|
46
|
+
.where('batch', batch)
|
|
47
|
+
.orderBy('id', 'desc')
|
|
48
|
+
.pluck('migration') as Promise<string[]>
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import type { DatabaseConnection } from '../contracts/Connection.ts'
|
|
2
|
+
import type { MigrationContract } from './Migration.ts'
|
|
3
|
+
import type { EventDispatcher } from '@mantiq/core'
|
|
4
|
+
import { MigrationRepository } from './MigrationRepository.ts'
|
|
5
|
+
import { MigrationStarted, MigrationEnded, MigrationsStarted, MigrationsEnded } from '../events/DatabaseEvents.ts'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
|
|
8
|
+
export interface MigrationFile {
|
|
9
|
+
name: string
|
|
10
|
+
migration: MigrationContract
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface MigratorOptions {
|
|
14
|
+
/** Directory containing migration files. Each file must default-export a Migration class. */
|
|
15
|
+
migrationsPath?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class Migrator {
|
|
19
|
+
private repo: MigrationRepository
|
|
20
|
+
|
|
21
|
+
/** Optional event dispatcher. Set by @mantiq/events when installed. */
|
|
22
|
+
static _dispatcher: EventDispatcher | null = null
|
|
23
|
+
|
|
24
|
+
constructor(
|
|
25
|
+
private readonly connection: DatabaseConnection,
|
|
26
|
+
private readonly options: MigratorOptions = {},
|
|
27
|
+
) {
|
|
28
|
+
this.repo = new MigrationRepository(connection)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async run(migrations?: MigrationFile[]): Promise<string[]> {
|
|
32
|
+
await this.repo.createTable()
|
|
33
|
+
|
|
34
|
+
const files = migrations ?? (await this.loadFromDirectory())
|
|
35
|
+
const ran = await this.repo.getRan()
|
|
36
|
+
const pending = files.filter((f) => !ran.includes(f.name))
|
|
37
|
+
|
|
38
|
+
if (!pending.length) return []
|
|
39
|
+
|
|
40
|
+
const batch = (await this.repo.getLastBatch()) + 1
|
|
41
|
+
const ran_: string[] = []
|
|
42
|
+
|
|
43
|
+
await Migrator._dispatcher?.emit(new MigrationsStarted('up'))
|
|
44
|
+
|
|
45
|
+
for (const file of pending) {
|
|
46
|
+
await Migrator._dispatcher?.emit(new MigrationStarted(file.name, 'up'))
|
|
47
|
+
const schema = this.connection.schema()
|
|
48
|
+
await file.migration.up(schema, this.connection)
|
|
49
|
+
await this.repo.log(file.name, batch)
|
|
50
|
+
ran_.push(file.name)
|
|
51
|
+
await Migrator._dispatcher?.emit(new MigrationEnded(file.name, 'up'))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
await Migrator._dispatcher?.emit(new MigrationsEnded('up'))
|
|
55
|
+
|
|
56
|
+
return ran_
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async rollback(migrations?: MigrationFile[]): Promise<string[]> {
|
|
60
|
+
await this.repo.createTable()
|
|
61
|
+
|
|
62
|
+
const lastBatch = await this.repo.getLastBatchMigrations()
|
|
63
|
+
if (!lastBatch.length) return []
|
|
64
|
+
|
|
65
|
+
// Build lookup map
|
|
66
|
+
const files = migrations ?? (await this.loadFromDirectory())
|
|
67
|
+
const lookup = new Map(files.map((f) => [f.name, f]))
|
|
68
|
+
|
|
69
|
+
const rolled: string[] = []
|
|
70
|
+
|
|
71
|
+
await Migrator._dispatcher?.emit(new MigrationsStarted('down'))
|
|
72
|
+
|
|
73
|
+
for (const name of lastBatch) {
|
|
74
|
+
const file = lookup.get(name)
|
|
75
|
+
if (!file) continue
|
|
76
|
+
await Migrator._dispatcher?.emit(new MigrationStarted(name, 'down'))
|
|
77
|
+
const schema = this.connection.schema()
|
|
78
|
+
await file.migration.down(schema, this.connection)
|
|
79
|
+
await this.repo.delete(name)
|
|
80
|
+
rolled.push(name)
|
|
81
|
+
await Migrator._dispatcher?.emit(new MigrationEnded(name, 'down'))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await Migrator._dispatcher?.emit(new MigrationsEnded('down'))
|
|
85
|
+
|
|
86
|
+
return rolled
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async reset(migrations?: MigrationFile[]): Promise<string[]> {
|
|
90
|
+
await this.repo.createTable()
|
|
91
|
+
|
|
92
|
+
const ran = await this.repo.getRan()
|
|
93
|
+
const files = migrations ?? (await this.loadFromDirectory())
|
|
94
|
+
const lookup = new Map(files.map((f) => [f.name, f]))
|
|
95
|
+
|
|
96
|
+
// Run in reverse order
|
|
97
|
+
const reversed = [...ran].reverse()
|
|
98
|
+
const rolled: string[] = []
|
|
99
|
+
|
|
100
|
+
await Migrator._dispatcher?.emit(new MigrationsStarted('down'))
|
|
101
|
+
|
|
102
|
+
for (const name of reversed) {
|
|
103
|
+
const file = lookup.get(name)
|
|
104
|
+
if (!file) continue
|
|
105
|
+
await Migrator._dispatcher?.emit(new MigrationStarted(name, 'down'))
|
|
106
|
+
const schema = this.connection.schema()
|
|
107
|
+
await file.migration.down(schema, this.connection)
|
|
108
|
+
await this.repo.delete(name)
|
|
109
|
+
rolled.push(name)
|
|
110
|
+
await Migrator._dispatcher?.emit(new MigrationEnded(name, 'down'))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await Migrator._dispatcher?.emit(new MigrationsEnded('down'))
|
|
114
|
+
|
|
115
|
+
return rolled
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Drop all tables and re-run all migrations from scratch.
|
|
120
|
+
*/
|
|
121
|
+
async fresh(migrations?: MigrationFile[]): Promise<string[]> {
|
|
122
|
+
const schema = this.connection.schema()
|
|
123
|
+
|
|
124
|
+
// Disable FK constraints so tables can be dropped in any order
|
|
125
|
+
await schema.disableForeignKeyConstraints()
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const tables = await this.getAllTables()
|
|
129
|
+
for (const table of tables) {
|
|
130
|
+
await schema.dropIfExists(table)
|
|
131
|
+
}
|
|
132
|
+
} finally {
|
|
133
|
+
await schema.enableForeignKeyConstraints()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Re-run all migrations
|
|
137
|
+
return this.run(migrations)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private async getAllTables(): Promise<string[]> {
|
|
141
|
+
const driver = this.connection.getDriverName()
|
|
142
|
+
let rows: Record<string, any>[]
|
|
143
|
+
|
|
144
|
+
if (driver === 'sqlite') {
|
|
145
|
+
rows = await this.connection.select(
|
|
146
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'",
|
|
147
|
+
)
|
|
148
|
+
return rows.map((r) => r['name'] as string)
|
|
149
|
+
} else if (driver === 'postgres') {
|
|
150
|
+
rows = await this.connection.select(
|
|
151
|
+
"SELECT tablename FROM pg_tables WHERE schemaname = 'public'",
|
|
152
|
+
)
|
|
153
|
+
return rows.map((r) => r['tablename'] as string)
|
|
154
|
+
} else if (driver === 'mssql') {
|
|
155
|
+
rows = await this.connection.select(
|
|
156
|
+
"SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE='BASE TABLE'",
|
|
157
|
+
)
|
|
158
|
+
return rows.map((r) => r['TABLE_NAME'] as string)
|
|
159
|
+
} else {
|
|
160
|
+
// MySQL
|
|
161
|
+
rows = await this.connection.select('SHOW TABLES')
|
|
162
|
+
return rows.map((r) => Object.values(r)[0] as string)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async status(): Promise<Array<{ name: string; ran: boolean; batch: number | null }>> {
|
|
167
|
+
await this.repo.createTable()
|
|
168
|
+
|
|
169
|
+
const files = await this.loadFromDirectory()
|
|
170
|
+
const ran = await this.repo.getRan()
|
|
171
|
+
const rows = await this.connection.table('migrations').get()
|
|
172
|
+
const batchMap = new Map(rows.map((r) => [r['migration'] as string, r['batch'] as number]))
|
|
173
|
+
|
|
174
|
+
return files.map((f) => ({
|
|
175
|
+
name: f.name,
|
|
176
|
+
ran: ran.includes(f.name),
|
|
177
|
+
batch: batchMap.get(f.name) ?? null,
|
|
178
|
+
}))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private async loadFromDirectory(): Promise<MigrationFile[]> {
|
|
182
|
+
if (!this.options.migrationsPath) return []
|
|
183
|
+
|
|
184
|
+
const glob = new Bun.Glob('*.ts')
|
|
185
|
+
const files: MigrationFile[] = []
|
|
186
|
+
|
|
187
|
+
for await (const file of glob.scan(this.options.migrationsPath)) {
|
|
188
|
+
const name = file.replace(/\.ts$/, '')
|
|
189
|
+
const path = join(this.options.migrationsPath, file)
|
|
190
|
+
const mod = await import(path)
|
|
191
|
+
const MigrationClass = mod.default
|
|
192
|
+
if (MigrationClass) {
|
|
193
|
+
files.push({ name, migration: new MigrationClass() })
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Sort by filename (timestamp prefix ensures correct order)
|
|
198
|
+
files.sort((a, b) => a.name.localeCompare(b.name))
|
|
199
|
+
return files
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import type { Model } from './Model.ts'
|
|
2
|
+
import { applyMacros } from '@mantiq/core'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Typed array wrapper for model query results.
|
|
6
|
+
* Provides convenience methods for working with sets of models.
|
|
7
|
+
*/
|
|
8
|
+
export class Collection<T extends Model> {
|
|
9
|
+
private items: T[]
|
|
10
|
+
|
|
11
|
+
constructor(items: T[] = []) {
|
|
12
|
+
this.items = [...items]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ── Array-like access ────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
get length(): number {
|
|
18
|
+
return this.items.length
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
[Symbol.iterator](): Iterator<T> {
|
|
22
|
+
return this.items[Symbol.iterator]()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
toArray(): T[] {
|
|
26
|
+
return [...this.items]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
all(): T[] {
|
|
30
|
+
return this.toArray()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ── Element access ──────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
first(): T | undefined {
|
|
36
|
+
return this.items[0]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
last(): T | undefined {
|
|
40
|
+
return this.items[this.items.length - 1]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get(index: number): T | undefined {
|
|
44
|
+
return this.items[index]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── Searching ────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
find(callback: (item: T) => boolean): T | undefined {
|
|
50
|
+
return this.items.find(callback)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
findByKey(id: any): T | undefined {
|
|
54
|
+
return this.items.find((item) => item.getKey() === id)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
contains(callback: ((item: T) => boolean) | any): boolean {
|
|
58
|
+
if (typeof callback === 'function') {
|
|
59
|
+
return this.items.some(callback as (item: T) => boolean)
|
|
60
|
+
}
|
|
61
|
+
return this.items.some((item) => item.getKey() === callback)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Filtering ────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
filter(callback: (item: T) => boolean): Collection<T> {
|
|
67
|
+
return new Collection(this.items.filter(callback))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
where(key: string, value: any): Collection<T> {
|
|
71
|
+
return this.filter((item) => item.getAttribute(key) === value)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
whereIn(key: string, values: any[]): Collection<T> {
|
|
75
|
+
return this.filter((item) => values.includes(item.getAttribute(key)))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
reject(callback: (item: T) => boolean): Collection<T> {
|
|
79
|
+
return this.filter((item) => !callback(item))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Transformation ──────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
map<U>(callback: (item: T, index: number) => U): U[] {
|
|
85
|
+
return this.items.map(callback)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
flatMap<U>(callback: (item: T) => U[]): U[] {
|
|
89
|
+
return this.items.flatMap(callback)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
pluck(key: string): any[] {
|
|
93
|
+
return this.items.map((item) => item.getAttribute(key))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
modelKeys(): any[] {
|
|
97
|
+
return this.items.map((item) => item.getKey())
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
unique(key?: string): Collection<T> {
|
|
101
|
+
const seen = new Set()
|
|
102
|
+
return this.filter((item) => {
|
|
103
|
+
const val = key ? item.getAttribute(key) : item.getKey()
|
|
104
|
+
if (seen.has(val)) return false
|
|
105
|
+
seen.add(val)
|
|
106
|
+
return true
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── Ordering ─────────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
sortBy(key: string, direction: 'asc' | 'desc' = 'asc'): Collection<T> {
|
|
113
|
+
const sorted = [...this.items].sort((a, b) => {
|
|
114
|
+
const aVal = a.getAttribute(key)
|
|
115
|
+
const bVal = b.getAttribute(key)
|
|
116
|
+
if (aVal < bVal) return direction === 'asc' ? -1 : 1
|
|
117
|
+
if (aVal > bVal) return direction === 'asc' ? 1 : -1
|
|
118
|
+
return 0
|
|
119
|
+
})
|
|
120
|
+
return new Collection(sorted)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
reverse(): Collection<T> {
|
|
124
|
+
return new Collection([...this.items].reverse())
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── Aggregates ──────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
count(): number {
|
|
130
|
+
return this.items.length
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
sum(key: string): number {
|
|
134
|
+
return this.items.reduce((acc, item) => acc + Number(item.getAttribute(key) ?? 0), 0)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
avg(key: string): number {
|
|
138
|
+
if (this.items.length === 0) return 0
|
|
139
|
+
return this.sum(key) / this.items.length
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
min(key: string): any {
|
|
143
|
+
if (this.items.length === 0) return undefined
|
|
144
|
+
return this.items.reduce((min, item) => {
|
|
145
|
+
const val = item.getAttribute(key)
|
|
146
|
+
return val < min ? val : min
|
|
147
|
+
}, this.items[0]!.getAttribute(key))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
max(key: string): any {
|
|
151
|
+
if (this.items.length === 0) return undefined
|
|
152
|
+
return this.items.reduce((max, item) => {
|
|
153
|
+
const val = item.getAttribute(key)
|
|
154
|
+
return val > max ? val : max
|
|
155
|
+
}, this.items[0]!.getAttribute(key))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── Chunking ─────────────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
chunk(size: number): Collection<T>[] {
|
|
161
|
+
const chunks: Collection<T>[] = []
|
|
162
|
+
for (let i = 0; i < this.items.length; i += size) {
|
|
163
|
+
chunks.push(new Collection(this.items.slice(i, i + size)))
|
|
164
|
+
}
|
|
165
|
+
return chunks
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── Side effects ────────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
each(callback: (item: T, index: number) => void): this {
|
|
171
|
+
this.items.forEach(callback)
|
|
172
|
+
return this
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── Grouping ─────────────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
groupBy(key: string): Map<any, Collection<T>> {
|
|
178
|
+
const groups = new Map<any, T[]>()
|
|
179
|
+
for (const item of this.items) {
|
|
180
|
+
const val = item.getAttribute(key)
|
|
181
|
+
if (!groups.has(val)) groups.set(val, [])
|
|
182
|
+
groups.get(val)!.push(item)
|
|
183
|
+
}
|
|
184
|
+
const result = new Map<any, Collection<T>>()
|
|
185
|
+
for (const [k, v] of groups) result.set(k, new Collection(v))
|
|
186
|
+
return result
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
keyBy(key: string): Map<any, T> {
|
|
190
|
+
const map = new Map<any, T>()
|
|
191
|
+
for (const item of this.items) {
|
|
192
|
+
map.set(item.getAttribute(key), item)
|
|
193
|
+
}
|
|
194
|
+
return map
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── Set operations ──────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
push(...items: T[]): this {
|
|
200
|
+
this.items.push(...items)
|
|
201
|
+
return this
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
concat(other: Collection<T> | T[]): Collection<T> {
|
|
205
|
+
const otherItems = other instanceof Collection ? other.toArray() : other
|
|
206
|
+
return new Collection([...this.items, ...otherItems])
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── Serialization ───────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
toJSON(): Record<string, any>[] {
|
|
212
|
+
return this.items.map((item) => item.toJSON())
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
isEmpty(): boolean {
|
|
216
|
+
return this.items.length === 0
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
isNotEmpty(): boolean {
|
|
220
|
+
return this.items.length > 0
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ── Batch operations ────────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
async load(...relations: string[]): Promise<this> {
|
|
226
|
+
// Lazy eager-load relations on an existing collection
|
|
227
|
+
// This delegates to the eager loading infrastructure
|
|
228
|
+
if (this.items.length === 0 || relations.length === 0) return this
|
|
229
|
+
const { eagerLoadRelations } = await import('./eagerLoad.ts')
|
|
230
|
+
await eagerLoadRelations(this.items, relations)
|
|
231
|
+
return this
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Add macro support — Collection.macro('name', fn) / instance.__macro('name')
|
|
236
|
+
applyMacros(Collection)
|