@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,170 @@
|
|
|
1
|
+
import type { Model, ModelStatic } from '../orm/Model.ts'
|
|
2
|
+
import { Faker } from './Faker.ts'
|
|
3
|
+
|
|
4
|
+
type DefinitionFn<T> = (index: number, fake: Faker) => Record<string, any>
|
|
5
|
+
type AfterCreateFn<T extends Model> = (model: T) => Promise<void>
|
|
6
|
+
|
|
7
|
+
export interface BulkCreateOptions {
|
|
8
|
+
/** Rows per INSERT statement (default 500) */
|
|
9
|
+
batchSize?: number
|
|
10
|
+
/** Called after each batch with rows inserted so far */
|
|
11
|
+
onProgress?: (inserted: number, total: number) => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Model factory for generating test data.
|
|
16
|
+
*
|
|
17
|
+
* Immutable — state(), count(), and afterCreate() return a new instance,
|
|
18
|
+
* so chaining never pollutes the original factory.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* class UserFactory extends Factory<User> {
|
|
22
|
+
* protected model = User
|
|
23
|
+
* definition(index: number, fake: Faker) {
|
|
24
|
+
* return {
|
|
25
|
+
* name: fake.name(),
|
|
26
|
+
* email: fake.email(),
|
|
27
|
+
* role: fake.pick(['admin', 'user']),
|
|
28
|
+
* }
|
|
29
|
+
* }
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* const factory = new UserFactory()
|
|
33
|
+
* const user = await factory.create()
|
|
34
|
+
* const users = await factory.count(5).create()
|
|
35
|
+
* const admin = await factory.state({ role: 'admin' }).create()
|
|
36
|
+
*/
|
|
37
|
+
export abstract class Factory<T extends Model> {
|
|
38
|
+
protected abstract model: ModelStatic<T>
|
|
39
|
+
protected _count = 1
|
|
40
|
+
protected _states: DefinitionFn<T>[] = []
|
|
41
|
+
protected _afterCreate: AfterCreateFn<T>[] = []
|
|
42
|
+
protected _sequence = 0
|
|
43
|
+
|
|
44
|
+
/** Shared faker instance — override or seed in subclass if needed */
|
|
45
|
+
protected fake = new Faker()
|
|
46
|
+
|
|
47
|
+
abstract definition(index: number, fake: Faker): Record<string, any>
|
|
48
|
+
|
|
49
|
+
/** Clone this factory with shallow-copied mutable arrays */
|
|
50
|
+
protected clone(): this {
|
|
51
|
+
const copy = Object.create(Object.getPrototypeOf(this)) as this
|
|
52
|
+
Object.assign(copy, this)
|
|
53
|
+
copy._states = [...this._states]
|
|
54
|
+
copy._afterCreate = [...this._afterCreate]
|
|
55
|
+
return copy
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
count(n: number): this {
|
|
59
|
+
const copy = this.clone()
|
|
60
|
+
copy._count = n
|
|
61
|
+
return copy
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
state(overrides: Partial<Record<string, any>> | DefinitionFn<T>): this {
|
|
65
|
+
const copy = this.clone()
|
|
66
|
+
if (typeof overrides === 'function') {
|
|
67
|
+
copy._states.push(overrides as DefinitionFn<T>)
|
|
68
|
+
} else {
|
|
69
|
+
copy._states.push(() => overrides)
|
|
70
|
+
}
|
|
71
|
+
return copy
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
afterCreate(fn: AfterCreateFn<T>): this {
|
|
75
|
+
const copy = this.clone()
|
|
76
|
+
copy._afterCreate.push(fn)
|
|
77
|
+
return copy
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Make model instances (not persisted) */
|
|
81
|
+
make(overrides?: Record<string, any>): T | T[] {
|
|
82
|
+
const results: T[] = []
|
|
83
|
+
for (let i = 0; i < this._count; i++) {
|
|
84
|
+
const index = ++this._sequence
|
|
85
|
+
const attrs = this.resolveAttributes(index, overrides)
|
|
86
|
+
const instance = new (this.model as any)()
|
|
87
|
+
instance.forceFill(attrs)
|
|
88
|
+
results.push(instance)
|
|
89
|
+
}
|
|
90
|
+
return this._count === 1 ? results[0]! : results
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Create and persist model instances */
|
|
94
|
+
async create(overrides?: Record<string, any>): Promise<T | T[]> {
|
|
95
|
+
const results: T[] = []
|
|
96
|
+
for (let i = 0; i < this._count; i++) {
|
|
97
|
+
const index = ++this._sequence
|
|
98
|
+
const attrs = this.resolveAttributes(index, overrides)
|
|
99
|
+
const model = await this.model.create(attrs)
|
|
100
|
+
for (const fn of this._afterCreate) await fn(model)
|
|
101
|
+
results.push(model)
|
|
102
|
+
}
|
|
103
|
+
return this._count === 1 ? results[0]! : results
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Create and return raw attribute objects (not persisted) */
|
|
107
|
+
raw(overrides?: Record<string, any>): Record<string, any> | Record<string, any>[] {
|
|
108
|
+
const results: Record<string, any>[] = []
|
|
109
|
+
for (let i = 0; i < this._count; i++) {
|
|
110
|
+
const index = ++this._sequence
|
|
111
|
+
results.push(this.resolveAttributes(index, overrides))
|
|
112
|
+
}
|
|
113
|
+
return this._count === 1 ? results[0]! : results
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Bulk-insert rows using multi-value INSERT statements in a transaction.
|
|
118
|
+
* Generates attributes in streaming batches to stay memory-friendly.
|
|
119
|
+
* Returns the total number of rows inserted.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* await new UserFactory().count(1_000_000).createBulk(db(), {
|
|
123
|
+
* batchSize: 1000,
|
|
124
|
+
* onProgress: (done, total) => console.log(`${done}/${total}`),
|
|
125
|
+
* })
|
|
126
|
+
*/
|
|
127
|
+
async createBulk(
|
|
128
|
+
connection: { statement(sql: string, bindings?: any[]): Promise<number>; transaction<R>(fn: (c: any) => Promise<R>): Promise<R> },
|
|
129
|
+
options?: BulkCreateOptions,
|
|
130
|
+
): Promise<number> {
|
|
131
|
+
const batchSize = options?.batchSize ?? 500
|
|
132
|
+
const total = this._count
|
|
133
|
+
const tableName = (this.model as any).table as string
|
|
134
|
+
let inserted = 0
|
|
135
|
+
|
|
136
|
+
await connection.transaction(async (conn: any) => {
|
|
137
|
+
while (inserted < total) {
|
|
138
|
+
const chunkSize = Math.min(batchSize, total - inserted)
|
|
139
|
+
const rows: Record<string, any>[] = []
|
|
140
|
+
|
|
141
|
+
for (let i = 0; i < chunkSize; i++) {
|
|
142
|
+
const index = ++this._sequence
|
|
143
|
+
rows.push(this.resolveAttributes(index))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const columns = Object.keys(rows[0]!)
|
|
147
|
+
const quotedCols = columns.map((c) => `"${c}"`).join(', ')
|
|
148
|
+
const rowPlaceholder = `(${columns.map(() => '?').join(', ')})`
|
|
149
|
+
const allPlaceholders = Array.from({ length: chunkSize }, () => rowPlaceholder).join(', ')
|
|
150
|
+
const bindings = rows.flatMap((r) => columns.map((c) => r[c]))
|
|
151
|
+
const sql = `INSERT INTO "${tableName}" (${quotedCols}) VALUES ${allPlaceholders}`
|
|
152
|
+
|
|
153
|
+
await conn.statement(sql, bindings)
|
|
154
|
+
inserted += chunkSize
|
|
155
|
+
options?.onProgress?.(inserted, total)
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
return inserted
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private resolveAttributes(index: number, overrides?: Record<string, any>): Record<string, any> {
|
|
163
|
+
let attrs = this.definition(index, this.fake)
|
|
164
|
+
for (const stateFn of this._states) {
|
|
165
|
+
attrs = { ...attrs, ...stateFn(index, this.fake) }
|
|
166
|
+
}
|
|
167
|
+
if (overrides) attrs = { ...attrs, ...overrides }
|
|
168
|
+
return attrs
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight fake data generator — zero dependencies.
|
|
3
|
+
*
|
|
4
|
+
* Covers the most common needs for seeding and testing without pulling in
|
|
5
|
+
* a heavyweight library like faker-js. Every method is deterministic when
|
|
6
|
+
* seeded, or random by default.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const fake = new Faker()
|
|
10
|
+
* fake.name() // "Liam Chen"
|
|
11
|
+
* fake.email() // "olivia.martinez42@example.com"
|
|
12
|
+
* fake.sentence() // "The quick brown fox jumps over the lazy dog."
|
|
13
|
+
*/
|
|
14
|
+
export class Faker {
|
|
15
|
+
private seed: number | null = null
|
|
16
|
+
private state: number = 0
|
|
17
|
+
|
|
18
|
+
constructor(seed?: number) {
|
|
19
|
+
if (seed !== undefined) {
|
|
20
|
+
this.seed = seed
|
|
21
|
+
this.state = seed
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── Random primitives ──────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
/** Random float in [0, 1) — seeded or Math.random() */
|
|
28
|
+
random(): number {
|
|
29
|
+
if (this.seed === null) return Math.random()
|
|
30
|
+
// xorshift32
|
|
31
|
+
this.state ^= this.state << 13
|
|
32
|
+
this.state ^= this.state >> 17
|
|
33
|
+
this.state ^= this.state << 5
|
|
34
|
+
return (this.state >>> 0) / 4294967296
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Random integer in [min, max] inclusive */
|
|
38
|
+
int(min = 0, max = 100): number {
|
|
39
|
+
return Math.floor(this.random() * (max - min + 1)) + min
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Random float in [min, max) */
|
|
43
|
+
float(min = 0, max = 1, decimals = 2): number {
|
|
44
|
+
const val = this.random() * (max - min) + min
|
|
45
|
+
const factor = 10 ** decimals
|
|
46
|
+
return Math.round(val * factor) / factor
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Random boolean with optional probability of true */
|
|
50
|
+
boolean(truthiness = 0.5): boolean {
|
|
51
|
+
return this.random() < truthiness
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Pick a random element from an array */
|
|
55
|
+
pick<T>(arr: readonly T[]): T {
|
|
56
|
+
return arr[this.int(0, arr.length - 1)]!
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Pick n unique elements from an array */
|
|
60
|
+
pickMultiple<T>(arr: readonly T[], count: number): T[] {
|
|
61
|
+
const shuffled = [...arr].sort(() => this.random() - 0.5)
|
|
62
|
+
return shuffled.slice(0, Math.min(count, arr.length))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Shuffle an array (returns new array) */
|
|
66
|
+
shuffle<T>(arr: readonly T[]): T[] {
|
|
67
|
+
const copy = [...arr]
|
|
68
|
+
for (let i = copy.length - 1; i > 0; i--) {
|
|
69
|
+
const j = this.int(0, i)
|
|
70
|
+
;[copy[i], copy[j]] = [copy[j]!, copy[i]!]
|
|
71
|
+
}
|
|
72
|
+
return copy
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Person ─────────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
firstName(): string {
|
|
78
|
+
return this.pick(FIRST_NAMES)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
lastName(): string {
|
|
82
|
+
return this.pick(LAST_NAMES)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
name(): string {
|
|
86
|
+
return `${this.firstName()} ${this.lastName()}`
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Random username: first.last + optional digits */
|
|
90
|
+
username(): string {
|
|
91
|
+
const first = this.firstName().toLowerCase()
|
|
92
|
+
const last = this.lastName().toLowerCase()
|
|
93
|
+
const suffix = this.boolean(0.6) ? String(this.int(1, 99)) : ''
|
|
94
|
+
return `${first}.${last}${suffix}`
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── Internet ───────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
email(domain?: string): string {
|
|
100
|
+
const d = domain ?? this.pick(EMAIL_DOMAINS)
|
|
101
|
+
return `${this.username()}@${d}`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
url(): string {
|
|
105
|
+
return `https://${this.pick(DOMAINS)}/${this.slug()}`
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
ip(): string {
|
|
109
|
+
return `${this.int(1, 255)}.${this.int(0, 255)}.${this.int(0, 255)}.${this.int(1, 254)}`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
ipv6(): string {
|
|
113
|
+
const segments: string[] = []
|
|
114
|
+
for (let i = 0; i < 8; i++) segments.push(this.int(0, 0xffff).toString(16).padStart(4, '0'))
|
|
115
|
+
return segments.join(':')
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Random hex color */
|
|
119
|
+
hexColor(): string {
|
|
120
|
+
return '#' + this.int(0, 0xffffff).toString(16).padStart(6, '0')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
userAgent(): string {
|
|
124
|
+
return this.pick(USER_AGENTS)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── Text ───────────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
word(): string {
|
|
130
|
+
return this.pick(WORDS)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
words(count?: number): string {
|
|
134
|
+
const n = count ?? this.int(2, 6)
|
|
135
|
+
return Array.from({ length: n }, () => this.word()).join(' ')
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
sentence(wordCount?: number): string {
|
|
139
|
+
const w = this.words(wordCount ?? this.int(5, 12))
|
|
140
|
+
return w.charAt(0).toUpperCase() + w.slice(1) + '.'
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
sentences(count?: number): string {
|
|
144
|
+
const n = count ?? this.int(2, 5)
|
|
145
|
+
return Array.from({ length: n }, () => this.sentence()).join(' ')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
paragraph(sentenceCount?: number): string {
|
|
149
|
+
return this.sentences(sentenceCount ?? this.int(3, 6))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
paragraphs(count?: number): string {
|
|
153
|
+
const n = count ?? this.int(2, 4)
|
|
154
|
+
return Array.from({ length: n }, () => this.paragraph()).join('\n\n')
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
slug(wordCount?: number): string {
|
|
158
|
+
return this.words(wordCount ?? this.int(2, 4)).replace(/\s+/g, '-')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── Numbers & IDs ──────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
uuid(): string {
|
|
164
|
+
const hex = () => this.int(0, 0xffff).toString(16).padStart(4, '0')
|
|
165
|
+
return `${hex()}${hex()}-${hex()}-4${hex().slice(1)}-${this.pick(['8', '9', 'a', 'b'])}${hex().slice(1)}-${hex()}${hex()}${hex()}`
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Numeric string of given length */
|
|
169
|
+
numericId(length = 8): string {
|
|
170
|
+
return Array.from({ length }, () => this.int(0, 9)).join('')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── Date & Time ────────────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
/** Random date between two dates */
|
|
176
|
+
date(from?: Date, to?: Date): Date {
|
|
177
|
+
const start = (from ?? new Date('2020-01-01')).getTime()
|
|
178
|
+
const end = (to ?? new Date()).getTime()
|
|
179
|
+
return new Date(this.int(start, end))
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** ISO date string */
|
|
183
|
+
dateString(from?: Date, to?: Date): string {
|
|
184
|
+
return this.date(from, to).toISOString()
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Recent date (within last N days, default 7) */
|
|
188
|
+
recent(days = 7): Date {
|
|
189
|
+
const now = Date.now()
|
|
190
|
+
return new Date(now - this.int(0, days * 86400000))
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Future date (within next N days, default 30) */
|
|
194
|
+
future(days = 30): Date {
|
|
195
|
+
const now = Date.now()
|
|
196
|
+
return new Date(now + this.int(1, days * 86400000))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── Address ────────────────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
city(): string {
|
|
202
|
+
return this.pick(CITIES)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
country(): string {
|
|
206
|
+
return this.pick(COUNTRIES)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
zipCode(): string {
|
|
210
|
+
return this.numericId(5)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
latitude(): number {
|
|
214
|
+
return this.float(-90, 90, 6)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
longitude(): number {
|
|
218
|
+
return this.float(-180, 180, 6)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── Company ────────────────────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
company(): string {
|
|
224
|
+
return this.pick(COMPANIES)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
jobTitle(): string {
|
|
228
|
+
return `${this.pick(JOB_LEVELS)} ${this.pick(JOB_AREAS)} ${this.pick(JOB_TYPES)}`
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ── Phone ──────────────────────────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
phone(): string {
|
|
234
|
+
return `+1${this.numericId(10)}`
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── Image / Avatar ─────────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
/** Placeholder avatar URL */
|
|
240
|
+
avatar(): string {
|
|
241
|
+
return `https://ui-avatars.com/api/?name=${encodeURIComponent(this.name())}&size=128`
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
imageUrl(width = 640, height = 480): string {
|
|
245
|
+
return `https://picsum.photos/seed/${this.int(1, 99999)}/${width}/${height}`
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
/** Generate a value from a pattern: # = digit, ? = lowercase letter, * = alphanumeric */
|
|
251
|
+
fromPattern(pattern: string): string {
|
|
252
|
+
let result = ''
|
|
253
|
+
for (const ch of pattern) {
|
|
254
|
+
if (ch === '#') result += String(this.int(0, 9))
|
|
255
|
+
else if (ch === '?') result += String.fromCharCode(this.int(97, 122))
|
|
256
|
+
else if (ch === '*') result += this.pick('abcdefghijklmnopqrstuvwxyz0123456789'.split(''))
|
|
257
|
+
else result += ch
|
|
258
|
+
}
|
|
259
|
+
return result
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Random hex string */
|
|
263
|
+
hex(length = 16): string {
|
|
264
|
+
return Array.from({ length }, () => this.int(0, 15).toString(16)).join('')
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Random alphanumeric string */
|
|
268
|
+
alphanumeric(length = 10): string {
|
|
269
|
+
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
|
270
|
+
return Array.from({ length }, () => chars[this.int(0, chars.length - 1)]).join('')
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Unique values: returns a proxy that deduplicates */
|
|
274
|
+
unique<K extends keyof this>(method: K, maxRetries = 100): this[K] {
|
|
275
|
+
const seen = new Set<any>()
|
|
276
|
+
const original = (this as any)[method]
|
|
277
|
+
if (typeof original !== 'function') throw new Error(`${String(method)} is not a method`)
|
|
278
|
+
return ((...args: any[]) => {
|
|
279
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
280
|
+
const val = original.call(this, ...args)
|
|
281
|
+
if (!seen.has(val)) { seen.add(val); return val }
|
|
282
|
+
}
|
|
283
|
+
throw new Error(`Faker.unique: could not generate unique ${String(method)} after ${maxRetries} tries`)
|
|
284
|
+
}) as any
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ── Data pools ──────────────────────────────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
const FIRST_NAMES = [
|
|
291
|
+
'Liam', 'Olivia', 'Noah', 'Emma', 'James', 'Sophia', 'Lucas', 'Ava',
|
|
292
|
+
'Mason', 'Isabella', 'Ethan', 'Mia', 'Alexander', 'Charlotte', 'Henry',
|
|
293
|
+
'Amelia', 'Sebastian', 'Harper', 'Jack', 'Evelyn', 'Daniel', 'Aria',
|
|
294
|
+
'Owen', 'Chloe', 'Samuel', 'Ella', 'Ryan', 'Scarlett', 'Leo', 'Grace',
|
|
295
|
+
'Nathan', 'Lily', 'Caleb', 'Layla', 'Isaac', 'Riley', 'Adam', 'Zoey',
|
|
296
|
+
'Dylan', 'Nora', 'Aiden', 'Hannah', 'Elijah', 'Stella', 'Logan', 'Luna',
|
|
297
|
+
'Gabriel', 'Penelope', 'Matthew', 'Violet',
|
|
298
|
+
] as const
|
|
299
|
+
|
|
300
|
+
const LAST_NAMES = [
|
|
301
|
+
'Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller',
|
|
302
|
+
'Davis', 'Rodriguez', 'Martinez', 'Hernandez', 'Lopez', 'Gonzalez',
|
|
303
|
+
'Wilson', 'Anderson', 'Thomas', 'Taylor', 'Moore', 'Jackson', 'Martin',
|
|
304
|
+
'Lee', 'Perez', 'Thompson', 'White', 'Harris', 'Sanchez', 'Clark',
|
|
305
|
+
'Ramirez', 'Lewis', 'Robinson', 'Walker', 'Young', 'Allen', 'King',
|
|
306
|
+
'Wright', 'Scott', 'Torres', 'Nguyen', 'Hill', 'Flores', 'Green',
|
|
307
|
+
'Adams', 'Nelson', 'Baker', 'Hall', 'Rivera', 'Campbell', 'Mitchell',
|
|
308
|
+
'Carter', 'Chen',
|
|
309
|
+
] as const
|
|
310
|
+
|
|
311
|
+
const EMAIL_DOMAINS = [
|
|
312
|
+
'gmail.com', 'yahoo.com', 'outlook.com', 'hotmail.com', 'proton.me',
|
|
313
|
+
'icloud.com', 'mail.com', 'example.com', 'test.com',
|
|
314
|
+
] as const
|
|
315
|
+
|
|
316
|
+
const DOMAINS = [
|
|
317
|
+
'example.com', 'test.org', 'sample.net', 'demo.io', 'mysite.dev',
|
|
318
|
+
] as const
|
|
319
|
+
|
|
320
|
+
const WORDS = [
|
|
321
|
+
'the', 'be', 'to', 'of', 'and', 'a', 'in', 'that', 'have', 'I',
|
|
322
|
+
'it', 'for', 'not', 'on', 'with', 'he', 'as', 'you', 'do', 'at',
|
|
323
|
+
'this', 'but', 'his', 'by', 'from', 'they', 'we', 'say', 'her',
|
|
324
|
+
'she', 'or', 'an', 'will', 'my', 'one', 'all', 'would', 'there',
|
|
325
|
+
'their', 'what', 'so', 'up', 'out', 'if', 'about', 'who', 'get',
|
|
326
|
+
'which', 'go', 'me', 'when', 'make', 'can', 'like', 'time', 'no',
|
|
327
|
+
'just', 'him', 'know', 'take', 'people', 'into', 'year', 'your',
|
|
328
|
+
'good', 'some', 'could', 'them', 'see', 'other', 'than', 'then',
|
|
329
|
+
'now', 'look', 'only', 'come', 'its', 'over', 'think', 'also',
|
|
330
|
+
'back', 'after', 'use', 'two', 'how', 'our', 'work', 'first',
|
|
331
|
+
'well', 'way', 'even', 'new', 'want', 'because', 'any', 'these',
|
|
332
|
+
'give', 'day', 'most', 'find', 'here', 'thing', 'many', 'right',
|
|
333
|
+
'large', 'great', 'system', 'part', 'small', 'number', 'place',
|
|
334
|
+
'point', 'home', 'hand', 'high', 'keep', 'last', 'long', 'world',
|
|
335
|
+
'school', 'still', 'study', 'every', 'start', 'might', 'story',
|
|
336
|
+
'city', 'open', 'build', 'group', 'local', 'state', 'power',
|
|
337
|
+
'data', 'team', 'cloud', 'pixel', 'orbit', 'spark', 'pulse',
|
|
338
|
+
'swift', 'bloom', 'forge', 'haven', 'ridge', 'ember', 'frost',
|
|
339
|
+
] as const
|
|
340
|
+
|
|
341
|
+
const CITIES = [
|
|
342
|
+
'New York', 'London', 'Tokyo', 'Paris', 'Berlin', 'Sydney', 'Toronto',
|
|
343
|
+
'Mumbai', 'São Paulo', 'Seoul', 'Dubai', 'Singapore', 'Amsterdam',
|
|
344
|
+
'Stockholm', 'San Francisco', 'Chicago', 'Los Angeles', 'Barcelona',
|
|
345
|
+
'Melbourne', 'Austin',
|
|
346
|
+
] as const
|
|
347
|
+
|
|
348
|
+
const COUNTRIES = [
|
|
349
|
+
'United States', 'United Kingdom', 'Canada', 'Australia', 'Germany',
|
|
350
|
+
'France', 'Japan', 'Brazil', 'India', 'South Korea', 'Netherlands',
|
|
351
|
+
'Sweden', 'Singapore', 'Spain', 'Italy', 'Mexico', 'Argentina',
|
|
352
|
+
'New Zealand', 'Switzerland', 'Portugal',
|
|
353
|
+
] as const
|
|
354
|
+
|
|
355
|
+
const COMPANIES = [
|
|
356
|
+
'Acme Corp', 'Globex', 'Initech', 'Hooli', 'Pied Piper', 'Stark Industries',
|
|
357
|
+
'Wayne Enterprises', 'Umbrella Corp', 'Soylent Corp', 'Tyrell Corp',
|
|
358
|
+
'Cyberdyne Systems', 'Massive Dynamic', 'Wonka Industries', 'Dunder Mifflin',
|
|
359
|
+
'Sterling Cooper', 'Weyland-Yutani', 'Aperture Science', 'Oscorp',
|
|
360
|
+
'LexCorp', 'Prestige Worldwide',
|
|
361
|
+
] as const
|
|
362
|
+
|
|
363
|
+
const JOB_LEVELS = [
|
|
364
|
+
'Senior', 'Junior', 'Lead', 'Principal', 'Staff', 'Associate', 'Chief',
|
|
365
|
+
] as const
|
|
366
|
+
|
|
367
|
+
const JOB_AREAS = [
|
|
368
|
+
'Software', 'Product', 'Design', 'Marketing', 'Sales', 'Data',
|
|
369
|
+
'DevOps', 'Security', 'Research', 'Operations',
|
|
370
|
+
] as const
|
|
371
|
+
|
|
372
|
+
const JOB_TYPES = [
|
|
373
|
+
'Engineer', 'Manager', 'Analyst', 'Architect', 'Consultant', 'Director',
|
|
374
|
+
'Specialist', 'Developer', 'Designer', 'Strategist',
|
|
375
|
+
] as const
|
|
376
|
+
|
|
377
|
+
const USER_AGENTS = [
|
|
378
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
379
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
380
|
+
'Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0',
|
|
381
|
+
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
|
|
382
|
+
] as const
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { DatabaseManager } from '../DatabaseManager.ts'
|
|
2
|
+
|
|
3
|
+
let _manager: DatabaseManager | null = null
|
|
4
|
+
|
|
5
|
+
export function setManager(manager: DatabaseManager): void {
|
|
6
|
+
_manager = manager
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getManager(): DatabaseManager {
|
|
10
|
+
if (!_manager) throw new Error('DatabaseManager not initialized. Call setManager() first.')
|
|
11
|
+
return _manager
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Get a SQL connection by name (or the default) */
|
|
15
|
+
export function db(connection?: string) {
|
|
16
|
+
return getManager().connection(connection)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Shorthand: query a table on the default connection */
|
|
20
|
+
export function table(name: string) {
|
|
21
|
+
return getManager().table(name)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Shorthand: get the schema builder on the default connection */
|
|
25
|
+
export function schema() {
|
|
26
|
+
return getManager().schema()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Get a MongoDB connection by name (or the default) */
|
|
30
|
+
export function mongo(connection?: string) {
|
|
31
|
+
return getManager().mongo(connection)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Shorthand: get a MongoDB collection */
|
|
35
|
+
export function collection(name: string) {
|
|
36
|
+
return getManager().collection(name)
|
|
37
|
+
}
|