ilana-orm 1.0.18 → 1.0.20
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 +133 -32
- package/cli/ilana.js +31 -145
- package/database/schema-builder.js +4 -0
- package/index.d.ts +8 -0
- package/index.edge.js +10 -0
- package/index.edge.mjs +38 -0
- package/index.js +2 -1
- package/index.mjs +1 -0
- package/orm/Errors.js +20 -1
- package/orm/Factory.js +2 -2
- package/orm/MigrationRunner.js +16 -48
- package/orm/Model.d.ts +128 -2
- package/orm/Model.js +187 -23
- package/orm/QueryBuilder.d.ts +10 -0
- package/orm/QueryBuilder.js +74 -1
- package/package.json +24 -7
package/orm/MigrationRunner.js
CHANGED
|
@@ -417,28 +417,20 @@ class MigrationRunner {
|
|
|
417
417
|
|
|
418
418
|
if (isCreate || name.includes('create_')) {
|
|
419
419
|
return isTS ?
|
|
420
|
-
`
|
|
421
|
-
|
|
422
|
-
export default class ${className} {
|
|
423
|
-
// connection = 'mysql'; // Uncomment to use specific connection
|
|
424
|
-
|
|
425
|
-
async up(schema: SchemaBuilder): Promise<void> {
|
|
420
|
+
`export default class ${className} {
|
|
421
|
+
async up(schema) {
|
|
426
422
|
await schema.createTable('${table}', (table) => {
|
|
427
423
|
table.increments('id');
|
|
428
424
|
table.timestamps();
|
|
429
425
|
});
|
|
430
426
|
}
|
|
431
427
|
|
|
432
|
-
async down(schema
|
|
428
|
+
async down(schema) {
|
|
433
429
|
await schema.dropTable('${table}');
|
|
434
430
|
}
|
|
435
431
|
}
|
|
436
432
|
` :
|
|
437
|
-
`
|
|
438
|
-
|
|
439
|
-
class ${className} {
|
|
440
|
-
// connection = 'mysql'; // Uncomment to use specific connection
|
|
441
|
-
|
|
433
|
+
`class ${className} {
|
|
442
434
|
async up(schema) {
|
|
443
435
|
await schema.createTable('${table}', (table) => {
|
|
444
436
|
table.increments('id');
|
|
@@ -455,42 +447,30 @@ module.exports = ${className};
|
|
|
455
447
|
`;
|
|
456
448
|
} else if (tableName) {
|
|
457
449
|
return isTS ?
|
|
458
|
-
`
|
|
459
|
-
|
|
460
|
-
export default class ${className} {
|
|
461
|
-
// connection = 'mysql'; // Uncomment to use specific connection
|
|
462
|
-
|
|
463
|
-
async up(schema: SchemaBuilder): Promise<void> {
|
|
450
|
+
`export default class ${className} {
|
|
451
|
+
async up(schema) {
|
|
464
452
|
await schema.table('${table}', (table) => {
|
|
465
|
-
//
|
|
466
|
-
// table.string('new_column').nullable();
|
|
453
|
+
// table.string('column_name').nullable();
|
|
467
454
|
});
|
|
468
455
|
}
|
|
469
456
|
|
|
470
|
-
async down(schema
|
|
457
|
+
async down(schema) {
|
|
471
458
|
await schema.table('${table}', (table) => {
|
|
472
|
-
//
|
|
473
|
-
// table.dropColumn('new_column');
|
|
459
|
+
// table.dropColumn('column_name');
|
|
474
460
|
});
|
|
475
461
|
}
|
|
476
462
|
}
|
|
477
463
|
` :
|
|
478
|
-
`
|
|
479
|
-
|
|
480
|
-
class ${className} {
|
|
481
|
-
// connection = 'mysql'; // Uncomment to use specific connection
|
|
482
|
-
|
|
464
|
+
`class ${className} {
|
|
483
465
|
async up(schema) {
|
|
484
466
|
await schema.table('${table}', (table) => {
|
|
485
|
-
//
|
|
486
|
-
// table.string('new_column').nullable();
|
|
467
|
+
// table.string('column_name').nullable();
|
|
487
468
|
});
|
|
488
469
|
}
|
|
489
470
|
|
|
490
471
|
async down(schema) {
|
|
491
472
|
await schema.table('${table}', (table) => {
|
|
492
|
-
//
|
|
493
|
-
// table.dropColumn('new_column');
|
|
473
|
+
// table.dropColumn('column_name');
|
|
494
474
|
});
|
|
495
475
|
}
|
|
496
476
|
}
|
|
@@ -500,31 +480,19 @@ module.exports = ${className};
|
|
|
500
480
|
}
|
|
501
481
|
|
|
502
482
|
return isTS ?
|
|
503
|
-
`
|
|
504
|
-
|
|
505
|
-
export default class ${className} {
|
|
506
|
-
// connection = 'mysql'; // Uncomment to use specific connection
|
|
507
|
-
|
|
508
|
-
async up(schema: SchemaBuilder): Promise<void> {
|
|
509
|
-
// Add your migration logic here
|
|
483
|
+
`export default class ${className} {
|
|
484
|
+
async up(schema) {
|
|
510
485
|
}
|
|
511
486
|
|
|
512
|
-
async down(schema
|
|
513
|
-
// Add your rollback logic here
|
|
487
|
+
async down(schema) {
|
|
514
488
|
}
|
|
515
489
|
}
|
|
516
490
|
` :
|
|
517
|
-
`
|
|
518
|
-
|
|
519
|
-
class ${className} {
|
|
520
|
-
// connection = 'mysql'; // Uncomment to use specific connection
|
|
521
|
-
|
|
491
|
+
`class ${className} {
|
|
522
492
|
async up(schema) {
|
|
523
|
-
// Add your migration logic here
|
|
524
493
|
}
|
|
525
494
|
|
|
526
495
|
async down(schema) {
|
|
527
|
-
// Add your rollback logic here
|
|
528
496
|
}
|
|
529
497
|
}
|
|
530
498
|
|
package/orm/Model.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import QueryBuilder from './QueryBuilder';
|
|
1
|
+
import QueryBuilder, { PaginationResult, SimplePaginationResult, CursorPaginationResult } from './QueryBuilder';
|
|
2
|
+
import Collection from './Collection';
|
|
2
3
|
import { HasOne, HasMany, BelongsTo, BelongsToMany, HasManyThrough, MorphTo, MorphOne, MorphMany } from './Relation';
|
|
3
4
|
|
|
4
5
|
export interface ModelAttributes {
|
|
@@ -36,7 +37,7 @@ export default class Model<TAttributes extends ModelAttributes = ModelAttributes
|
|
|
36
37
|
protected static table: string;
|
|
37
38
|
protected static connection?: string;
|
|
38
39
|
protected static primaryKey: string;
|
|
39
|
-
protected static keyType: 'number' | 'string';
|
|
40
|
+
protected static keyType: 'number' | 'string' | 'uuid' | 'ulid';
|
|
40
41
|
protected static incrementing: boolean;
|
|
41
42
|
protected static timestamps: boolean;
|
|
42
43
|
protected static createdAt: string;
|
|
@@ -51,8 +52,12 @@ export default class Model<TAttributes extends ModelAttributes = ModelAttributes
|
|
|
51
52
|
protected static appends: string[];
|
|
52
53
|
protected static timezone: string;
|
|
53
54
|
static strictLoading: boolean;
|
|
55
|
+
static preventsSilentlyDiscardingAttributes: boolean;
|
|
54
56
|
static touches: string[];
|
|
55
57
|
static enums: { [column: string]: string[] };
|
|
58
|
+
static embeddingColumn: string;
|
|
59
|
+
static embeddingDimensions: number;
|
|
60
|
+
static embeddingProvider?: (text: string) => Promise<number[]>;
|
|
56
61
|
|
|
57
62
|
// Instance properties
|
|
58
63
|
attributes: ModelAttributes;
|
|
@@ -72,6 +77,7 @@ export default class Model<TAttributes extends ModelAttributes = ModelAttributes
|
|
|
72
77
|
|
|
73
78
|
// Static methods
|
|
74
79
|
static register(): void;
|
|
80
|
+
static _autoRegister(): void;
|
|
75
81
|
static resolveRelatedModel(related: string | typeof Model): typeof Model;
|
|
76
82
|
static query(): QueryBuilder;
|
|
77
83
|
static with(...relations: string[]): QueryBuilder;
|
|
@@ -86,19 +92,136 @@ export default class Model<TAttributes extends ModelAttributes = ModelAttributes
|
|
|
86
92
|
static oldest(column?: string): QueryBuilder;
|
|
87
93
|
static withTrashed(): QueryBuilder;
|
|
88
94
|
static onlyTrashed(): QueryBuilder;
|
|
95
|
+
static withoutTrashed(): QueryBuilder;
|
|
96
|
+
static findOrFail(id: number | string): Promise<Model>;
|
|
97
|
+
static insertGetId(data: { [key: string]: any }): Promise<number | string>;
|
|
89
98
|
static upsert(data: any[], uniqueBy: string[], update?: string[]): Promise<any>;
|
|
90
99
|
static withoutGlobalScopes(): QueryBuilder;
|
|
91
100
|
static make(attributes?: ModelAttributes): Model;
|
|
92
101
|
static create(attributes?: ModelAttributes): Promise<Model>;
|
|
93
102
|
static generateUuid(): string;
|
|
103
|
+
static generateUlid(): string;
|
|
104
|
+
static _generateKey(): string;
|
|
105
|
+
static withoutEvents<T>(callback: () => Promise<T>): Promise<T>;
|
|
106
|
+
static prunable(): QueryBuilder;
|
|
107
|
+
static prune(): Promise<number>;
|
|
94
108
|
static insert(data: ModelAttributes | ModelAttributes[]): Promise<any>;
|
|
95
109
|
static destroy(ids: any | any[]): Promise<number>;
|
|
96
110
|
static truncate(): Promise<void>;
|
|
97
111
|
static seed(count?: number): Promise<any[]>;
|
|
112
|
+
static nearestTo(vector: number[], options?: { limit?: number; column?: string; distance?: 'cosine' | 'l2' | 'inner' }): Promise<Collection<any>>;
|
|
113
|
+
static search(text: string, options?: { limit?: number; column?: string; distance?: 'cosine' | 'l2' | 'inner'; provider?: (text: string) => Promise<number[]> }): Promise<Collection<any>>;
|
|
98
114
|
static firstOrCreate(attributes: ModelAttributes, values?: ModelAttributes): Promise<Model>;
|
|
99
115
|
static firstOrNew(attributes: ModelAttributes, values?: ModelAttributes): Promise<Model>;
|
|
100
116
|
static updateOrCreate(attributes: ModelAttributes, values?: ModelAttributes): Promise<Model>;
|
|
101
117
|
|
|
118
|
+
// QueryBuilder methods forwarded from Model.query() — at runtime, any
|
|
119
|
+
// QueryBuilder method not already declared above is proxied automatically
|
|
120
|
+
// from Model.<method>() to Model.query().<method>() (see Model.js), so this
|
|
121
|
+
// list exists purely to give TypeScript users type information for it.
|
|
122
|
+
static where(column: string, value: any): QueryBuilder;
|
|
123
|
+
static where(column: string, operator: string, value: any): QueryBuilder;
|
|
124
|
+
static orWhere(column: string, value: any): QueryBuilder;
|
|
125
|
+
static orWhere(column: string, operator: string, value: any): QueryBuilder;
|
|
126
|
+
static whereIn(column: string, values: any[]): QueryBuilder;
|
|
127
|
+
static whereNotIn(column: string, values: any[]): QueryBuilder;
|
|
128
|
+
static whereNull(column: string): QueryBuilder;
|
|
129
|
+
static whereNotNull(column: string): QueryBuilder;
|
|
130
|
+
static whereBetween(column: string, range: [any, any]): QueryBuilder;
|
|
131
|
+
static whereNotBetween(column: string, range: [any, any]): QueryBuilder;
|
|
132
|
+
static whereJsonContains(column: string, value: any): QueryBuilder;
|
|
133
|
+
static whereJsonLength(column: string, operator: string, value: number): QueryBuilder;
|
|
134
|
+
static whereDate(column: string, value: string): QueryBuilder;
|
|
135
|
+
static whereDate(column: string, operator: string, value: string): QueryBuilder;
|
|
136
|
+
static whereMonth(column: string, month: number): QueryBuilder;
|
|
137
|
+
static whereYear(column: string, year: number): QueryBuilder;
|
|
138
|
+
static whereDay(column: string, operatorOrValue: any, value?: any): QueryBuilder;
|
|
139
|
+
static whereTime(column: string, operatorOrValue: any, value?: any): QueryBuilder;
|
|
140
|
+
static whereRaw(sql: string, bindings?: any[]): QueryBuilder;
|
|
141
|
+
static orWhereNull(column: string): QueryBuilder;
|
|
142
|
+
static orWhereNotNull(column: string): QueryBuilder;
|
|
143
|
+
static orWhereIn(column: string, values: any[]): QueryBuilder;
|
|
144
|
+
static orWhereNotIn(column: string, values: any[]): QueryBuilder;
|
|
145
|
+
static orWhereRaw(sql: string, bindings?: any[]): QueryBuilder;
|
|
146
|
+
static whereExists(callback: (query: QueryBuilder) => void): QueryBuilder;
|
|
147
|
+
static whereNotExists(callback: (query: QueryBuilder) => void): QueryBuilder;
|
|
148
|
+
static when<T>(condition: T, callback: (query: QueryBuilder, condition: T) => void, otherwise?: (query: QueryBuilder) => void): QueryBuilder;
|
|
149
|
+
static unless<T>(condition: T, callback: (query: QueryBuilder) => void, otherwise?: (query: QueryBuilder, condition: T) => void): QueryBuilder;
|
|
150
|
+
|
|
151
|
+
static join(table: string, first: string, operator: string, second: string): QueryBuilder;
|
|
152
|
+
static leftJoin(table: string, first: string, operator: string, second: string): QueryBuilder;
|
|
153
|
+
static rightJoin(table: string, first: string, operator: string, second: string): QueryBuilder;
|
|
154
|
+
static innerJoin(table: string, first: string, operator: string, second: string): QueryBuilder;
|
|
155
|
+
static crossJoin(table: string): QueryBuilder;
|
|
156
|
+
|
|
157
|
+
static orderBy(column: string, direction?: 'asc' | 'desc'): QueryBuilder;
|
|
158
|
+
static orderByRaw(sql: string): QueryBuilder;
|
|
159
|
+
static orderBySubquery(callback: (query: QueryBuilder) => void, direction?: 'asc' | 'desc'): QueryBuilder;
|
|
160
|
+
static inRandomOrder(): QueryBuilder;
|
|
161
|
+
static limit(count: number): QueryBuilder;
|
|
162
|
+
static offset(count: number): QueryBuilder;
|
|
163
|
+
static take(count: number): QueryBuilder;
|
|
164
|
+
static skip(count: number): QueryBuilder;
|
|
165
|
+
static from(table: string): QueryBuilder;
|
|
166
|
+
static forPage(page: number, perPage?: number): QueryBuilder;
|
|
167
|
+
|
|
168
|
+
static groupBy(...columns: string[]): QueryBuilder;
|
|
169
|
+
static having(column: string, operator: string, value: any): QueryBuilder;
|
|
170
|
+
static having(rawSql: string): QueryBuilder;
|
|
171
|
+
static havingRaw(sql: string, bindings?: any[]): QueryBuilder;
|
|
172
|
+
|
|
173
|
+
static lockForUpdate(): QueryBuilder;
|
|
174
|
+
static sharedLock(): QueryBuilder;
|
|
175
|
+
static skipLocked(): QueryBuilder;
|
|
176
|
+
static noWait(): QueryBuilder;
|
|
177
|
+
|
|
178
|
+
static select(...columns: any[]): QueryBuilder;
|
|
179
|
+
static addSelect(...columns: any[]): QueryBuilder;
|
|
180
|
+
static addSelect(subqueries: { [alias: string]: (query: QueryBuilder) => void }): QueryBuilder;
|
|
181
|
+
static distinct(): QueryBuilder;
|
|
182
|
+
static selectRaw(sql: string, bindings?: any[]): QueryBuilder;
|
|
183
|
+
|
|
184
|
+
static withPendingAttributes(attributes: { [key: string]: any }): QueryBuilder;
|
|
185
|
+
static withConstraints(relation: string, callback: (query: QueryBuilder) => void): QueryBuilder;
|
|
186
|
+
static withConstraints(relations: { [key: string]: (query: QueryBuilder) => void }): QueryBuilder;
|
|
187
|
+
static whereHas(relation: string, callback?: (query: QueryBuilder) => void): QueryBuilder;
|
|
188
|
+
static doesntHave(relation: string): QueryBuilder;
|
|
189
|
+
static whereDoesntHave(relation: string, callback?: (query: QueryBuilder) => void): QueryBuilder;
|
|
190
|
+
static has(relation: string, operator?: '=' | '!=' | '<' | '<=' | '>' | '>=', count?: number): QueryBuilder;
|
|
191
|
+
|
|
192
|
+
static count(column?: string): Promise<number>;
|
|
193
|
+
static sum(column: string): Promise<number>;
|
|
194
|
+
static avg(column: string): Promise<number>;
|
|
195
|
+
static min(column: string): Promise<any>;
|
|
196
|
+
static max(column: string): Promise<any>;
|
|
197
|
+
|
|
198
|
+
static pluck(column: string): Promise<any[]>;
|
|
199
|
+
static exists(): Promise<boolean>;
|
|
200
|
+
static doesntExist(): Promise<boolean>;
|
|
201
|
+
static sole(): Promise<Model>;
|
|
202
|
+
static tap(callback: (query: QueryBuilder) => void): QueryBuilder;
|
|
203
|
+
static get(): Promise<Collection<Model>>;
|
|
204
|
+
|
|
205
|
+
static paginate(page?: number, perPage?: number): Promise<PaginationResult<Model>>;
|
|
206
|
+
static simplePaginate(page?: number, perPage?: number): Promise<SimplePaginationResult<Model>>;
|
|
207
|
+
static cursorPaginate(perPage?: number, cursor?: string, column?: string, direction?: 'asc' | 'desc'): Promise<CursorPaginationResult<Model>>;
|
|
208
|
+
|
|
209
|
+
static chunk(size: number, callback: (models: Collection<Model>) => Promise<void>): Promise<void>;
|
|
210
|
+
static cursor(chunkSize?: number): AsyncGenerator<Model, void, unknown>;
|
|
211
|
+
static lazy(chunkSize?: number): AsyncGenerator<Model, void, unknown>;
|
|
212
|
+
|
|
213
|
+
static update(data: any): Promise<number>;
|
|
214
|
+
static increment(column: string, amount?: number): Promise<number>;
|
|
215
|
+
static decrement(column: string, amount?: number): Promise<number>;
|
|
216
|
+
static delete(): Promise<number>;
|
|
217
|
+
static restore(): Promise<number>;
|
|
218
|
+
|
|
219
|
+
static clone(): QueryBuilder;
|
|
220
|
+
static toKnex(): any;
|
|
221
|
+
static toSql(): string;
|
|
222
|
+
static values(): Promise<any[]>;
|
|
223
|
+
static new(attributes?: { [key: string]: any }): Promise<Model>;
|
|
224
|
+
|
|
102
225
|
// Scopes
|
|
103
226
|
static addGlobalScope(name: string, scope: (query: QueryBuilder) => void): void;
|
|
104
227
|
static removeGlobalScope(name: string): void;
|
|
@@ -129,6 +252,7 @@ export default class Model<TAttributes extends ModelAttributes = ModelAttributes
|
|
|
129
252
|
// Instance methods
|
|
130
253
|
getKey(): any;
|
|
131
254
|
fill(attributes: ModelAttributes): this;
|
|
255
|
+
forceFill(attributes: ModelAttributes): this;
|
|
132
256
|
load(...relations: string[]): Promise<this>;
|
|
133
257
|
loadMissing(...relations: string[]): Promise<this>;
|
|
134
258
|
getRelation(key: string): any;
|
|
@@ -156,6 +280,8 @@ export default class Model<TAttributes extends ModelAttributes = ModelAttributes
|
|
|
156
280
|
forceDelete(): Promise<boolean>;
|
|
157
281
|
fresh(): Promise<this | null>;
|
|
158
282
|
is(other: Model): boolean;
|
|
283
|
+
isNot(other: Model | null | undefined): boolean;
|
|
284
|
+
replicate(except?: string[]): this;
|
|
159
285
|
toJSON(): any;
|
|
160
286
|
|
|
161
287
|
// Relationships
|
package/orm/Model.js
CHANGED
|
@@ -3,21 +3,24 @@ const QueryBuilder = require('./QueryBuilder');
|
|
|
3
3
|
const { HasOne, HasMany, BelongsTo, BelongsToMany, HasManyThrough, MorphTo, MorphMany, MorphOne } = require('./Relation');
|
|
4
4
|
const ModelRegistry = require('./ModelRegistry');
|
|
5
5
|
const Database = require('../database/connection');
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
6
|
+
const { MassAssignmentException } = require('./Errors');
|
|
7
|
+
|
|
8
|
+
// Auto-load configuration on first import (skipped in edge runtime)
|
|
9
|
+
if (typeof process !== 'undefined' && process.versions && process.versions.node && !global.__ILANA_EDGE__) {
|
|
10
|
+
(function autoLoadConfig() {
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const configPathJs = path.join(process.cwd(), 'ilana.config.js');
|
|
14
|
+
const configPathMjs = path.join(process.cwd(), 'ilana.config.mjs');
|
|
15
|
+
|
|
16
|
+
if (fs.existsSync(configPathJs)) {
|
|
17
|
+
delete require.cache[configPathJs];
|
|
18
|
+
require(configPathJs);
|
|
19
|
+
} else if (fs.existsSync(configPathMjs)) {
|
|
20
|
+
// For ES modules, handled in _getConfig
|
|
21
|
+
}
|
|
22
|
+
})();
|
|
23
|
+
}
|
|
21
24
|
|
|
22
25
|
class Model {
|
|
23
26
|
// --- Static defaults ---
|
|
@@ -36,8 +39,11 @@ class Model {
|
|
|
36
39
|
static appends = [];
|
|
37
40
|
static timezone = 'UTC';
|
|
38
41
|
static strictLoading = false;
|
|
42
|
+
static preventsSilentlyDiscardingAttributes = false;
|
|
39
43
|
static touches = [];
|
|
40
44
|
static enums = {};
|
|
45
|
+
static embeddingColumn = 'embedding';
|
|
46
|
+
static embeddingDimensions = 1536;
|
|
41
47
|
|
|
42
48
|
// --- Instance props ---
|
|
43
49
|
attributes = {};
|
|
@@ -53,6 +59,8 @@ class Model {
|
|
|
53
59
|
_deferred;
|
|
54
60
|
|
|
55
61
|
constructor(attrs = {}) {
|
|
62
|
+
this.constructor._autoRegister();
|
|
63
|
+
|
|
56
64
|
// instance-level fillable/guarded/casts
|
|
57
65
|
this.fillable = Array.isArray(this.fillable) && this.fillable.length
|
|
58
66
|
? this.fillable
|
|
@@ -164,10 +172,21 @@ class Model {
|
|
|
164
172
|
ModelRegistry.register(this.name, this);
|
|
165
173
|
}
|
|
166
174
|
|
|
175
|
+
// Registers this class the first time it's actually used (constructed or
|
|
176
|
+
// queried), so string-based relations (this.hasMany('Post')) resolve
|
|
177
|
+
// without requiring an explicit Post.register() call, as long as Post gets
|
|
178
|
+
// used somewhere before the relation is loaded. Cheap to call repeatedly:
|
|
179
|
+
// skips the registry write once this class is already registered as itself.
|
|
180
|
+
static _autoRegister() {
|
|
181
|
+
if (ModelRegistry.get(this.name) !== this) {
|
|
182
|
+
ModelRegistry.register(this.name, this);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
167
186
|
static resolveRelatedModel(related) {
|
|
168
187
|
if (typeof related === 'string') {
|
|
169
188
|
const cls = ModelRegistry.get(related);
|
|
170
|
-
if (!cls) throw new Error(`Model '${related}' not found.
|
|
189
|
+
if (!cls) throw new Error(`Model '${related}' not found. It gets registered automatically the first time it's queried or constructed, so require/use ${related} somewhere before this point, or call ${related}.register() explicitly.`);
|
|
171
190
|
return cls;
|
|
172
191
|
}
|
|
173
192
|
|
|
@@ -189,6 +208,7 @@ class Model {
|
|
|
189
208
|
|
|
190
209
|
// --- Query builder ---
|
|
191
210
|
static query() {
|
|
211
|
+
this._autoRegister();
|
|
192
212
|
|
|
193
213
|
const qb = new QueryBuilder(this.getTableName(), this, this.getConnectionName());
|
|
194
214
|
|
|
@@ -225,6 +245,9 @@ class Model {
|
|
|
225
245
|
static oldest(col) { return this.query().oldest(col || this.createdAt || 'created_at'); }
|
|
226
246
|
static withTrashed() { return this.query().withTrashed(); }
|
|
227
247
|
static onlyTrashed() { return this.query().onlyTrashed(); }
|
|
248
|
+
static withoutTrashed() { return this.query().withoutTrashed(); }
|
|
249
|
+
static async findOrFail(id) { return this.query().findOrFail(id); }
|
|
250
|
+
static async insertGetId(data) { return this.query().insertGetId(data); }
|
|
228
251
|
static async upsert(data, uniqueBy, update) { return this.query().upsert(data, uniqueBy, update); }
|
|
229
252
|
static withoutGlobalScopes() {
|
|
230
253
|
return new QueryBuilder(this.getTableName(), this, this.getConnectionName());
|
|
@@ -233,8 +256,8 @@ class Model {
|
|
|
233
256
|
static make(attrs = {}) {
|
|
234
257
|
const inst = new this(attrs);
|
|
235
258
|
inst._initialize();
|
|
236
|
-
if (!this.incrementing && this.keyType === 'string' && !inst.getKey()) {
|
|
237
|
-
inst.setAttribute(this.primaryKey, this.
|
|
259
|
+
if (!this.incrementing && (this.keyType === 'string' || this.keyType === 'uuid' || this.keyType === 'ulid') && !inst.getKey()) {
|
|
260
|
+
inst.setAttribute(this.primaryKey, this._generateKey());
|
|
238
261
|
}
|
|
239
262
|
return inst;
|
|
240
263
|
}
|
|
@@ -252,8 +275,63 @@ class Model {
|
|
|
252
275
|
});
|
|
253
276
|
}
|
|
254
277
|
|
|
278
|
+
static generateUlid() {
|
|
279
|
+
const CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
280
|
+
const now = Date.now();
|
|
281
|
+
let t = now;
|
|
282
|
+
let ts = '';
|
|
283
|
+
for (let i = 9; i >= 0; i--) {
|
|
284
|
+
ts = CHARS[t % 32] + ts;
|
|
285
|
+
t = Math.floor(t / 32);
|
|
286
|
+
}
|
|
287
|
+
let rand = '';
|
|
288
|
+
for (let i = 0; i < 16; i++) rand += CHARS[Math.floor(Math.random() * 32)];
|
|
289
|
+
return ts + rand;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
static _generateKey() {
|
|
293
|
+
if (this.keyType === 'ulid') return this.generateUlid();
|
|
294
|
+
return this.generateUuid();
|
|
295
|
+
}
|
|
296
|
+
|
|
255
297
|
static async insert(data) { return this.query().insert(data); }
|
|
256
298
|
static async truncate() { return this.query().toKnex().truncate(); }
|
|
299
|
+
|
|
300
|
+
static _assertPgVector() {
|
|
301
|
+
const conn = Database.connection(this.connection);
|
|
302
|
+
const client = conn?.client?.config?.client || '';
|
|
303
|
+
if (!client.includes('pg')) {
|
|
304
|
+
throw new Error(
|
|
305
|
+
`${this.name}.search() and ${this.name}.nearestTo() require PostgreSQL with the pgvector extension. ` +
|
|
306
|
+
`Current database client is '${client || 'unknown'}'. ` +
|
|
307
|
+
`Vector search is not supported on MySQL or SQLite.`
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
static async nearestTo(vector, { limit = 10, column, distance = 'cosine' } = {}) {
|
|
313
|
+
this._assertPgVector();
|
|
314
|
+
const col = column || this.embeddingColumn;
|
|
315
|
+
const ops = { cosine: '<=>', l2: '<->', inner: '<#>' };
|
|
316
|
+
const op = ops[distance] || '<=>';
|
|
317
|
+
const vectorStr = `[${Array.from(vector).join(',')}]`;
|
|
318
|
+
return this.query()
|
|
319
|
+
.selectRaw(`*, (${col} ${op} ?) as distance`, [vectorStr])
|
|
320
|
+
.orderByRaw(`${col} ${op} ?`, [vectorStr])
|
|
321
|
+
.limit(limit)
|
|
322
|
+
.get();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
static async search(text, { limit = 10, column, distance = 'cosine', provider } = {}) {
|
|
326
|
+
this._assertPgVector();
|
|
327
|
+
const embed = provider || this.embeddingProvider;
|
|
328
|
+
if (!embed) throw new Error(
|
|
329
|
+
`${this.name}.search() requires an embedding provider. ` +
|
|
330
|
+
`Pass { provider: async (text) => number[] } or set ${this.name}.embeddingProvider.`
|
|
331
|
+
);
|
|
332
|
+
const vector = await embed(text);
|
|
333
|
+
return this.nearestTo(vector, { limit, column, distance });
|
|
334
|
+
}
|
|
257
335
|
static async seed(count = 1) {
|
|
258
336
|
const { factory } = require('./Factory');
|
|
259
337
|
return factory(this).times(count).create();
|
|
@@ -341,6 +419,7 @@ class Model {
|
|
|
341
419
|
}
|
|
342
420
|
// static async fireEvent(evt, mdl) { for (const h of this.events[evt] || []) if (await h(mdl) === false) return false; }
|
|
343
421
|
static async fireEvent(evt, mdl) {
|
|
422
|
+
if (this._mutingEvents) return true;
|
|
344
423
|
const ownEvents = Object.hasOwn(this, 'events') ? this.events : {};
|
|
345
424
|
const handlers = ownEvents[evt] || [];
|
|
346
425
|
for (const handler of handlers) {
|
|
@@ -349,6 +428,31 @@ class Model {
|
|
|
349
428
|
return true;
|
|
350
429
|
}
|
|
351
430
|
|
|
431
|
+
static async withoutEvents(callback) {
|
|
432
|
+
this._mutingEvents = true;
|
|
433
|
+
try {
|
|
434
|
+
return await callback();
|
|
435
|
+
} finally {
|
|
436
|
+
this._mutingEvents = false;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
static prunable() {
|
|
441
|
+
throw new Error(`${this.name} must implement a static prunable() method that returns a QueryBuilder.`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
static async prune() {
|
|
445
|
+
const query = this.prunable();
|
|
446
|
+
let pruned = 0;
|
|
447
|
+
await query.chunk(1000, async (models) => {
|
|
448
|
+
for (const model of models) {
|
|
449
|
+
await model.delete();
|
|
450
|
+
pruned++;
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
return pruned;
|
|
454
|
+
}
|
|
455
|
+
|
|
352
456
|
// --- Instance methods ---
|
|
353
457
|
static getTableName() { return this.table || this.name.toLowerCase() + 's'; }
|
|
354
458
|
static getPrimaryKey() { return this.primaryKey; }
|
|
@@ -359,8 +463,29 @@ class Model {
|
|
|
359
463
|
getKey() { return this.attributes[this.constructor.primaryKey]; }
|
|
360
464
|
|
|
361
465
|
fill(attrs) {
|
|
466
|
+
const discarded = [];
|
|
467
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
468
|
+
if (!this.isFillable(k)) { discarded.push(k); continue; }
|
|
469
|
+
this.setAttribute(k, v);
|
|
470
|
+
}
|
|
471
|
+
// Off by default, matching every prior release: a key rejected by
|
|
472
|
+
// fillable/guarded is silently dropped, same as always. Opt in per-model
|
|
473
|
+
// with `static preventsSilentlyDiscardingAttributes = true` to instead
|
|
474
|
+
// throw on any discarded key — useful for catching a forgotten
|
|
475
|
+
// `fillable` declaration or a typo'd column name in update() calls,
|
|
476
|
+
// without changing behavior for anyone who hasn't asked for it.
|
|
477
|
+
if (discarded.length > 0 && this.constructor.preventsSilentlyDiscardingAttributes) {
|
|
478
|
+
throw new MassAssignmentException(this.constructor.name, discarded);
|
|
479
|
+
}
|
|
480
|
+
return this;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Sets attributes bypassing fillable/guarded entirely, for trusted,
|
|
484
|
+
// programmatic data (factories, seeders, internal code) rather than
|
|
485
|
+
// user-supplied mass assignment — mirrors fill()'s counterpart in
|
|
486
|
+
// Eloquent-style ORMs.
|
|
487
|
+
forceFill(attrs) {
|
|
362
488
|
for (const [k, v] of Object.entries(attrs)) {
|
|
363
|
-
if (!this.isFillable(k)) continue;
|
|
364
489
|
this.setAttribute(k, v);
|
|
365
490
|
}
|
|
366
491
|
return this;
|
|
@@ -531,9 +656,10 @@ class Model {
|
|
|
531
656
|
this.setAttribute(createdAtCol, now).setAttribute(updatedAtCol, now);
|
|
532
657
|
}
|
|
533
658
|
|
|
534
|
-
// Generate UUID if needed
|
|
535
|
-
|
|
536
|
-
|
|
659
|
+
// Generate UUID/ULID if needed
|
|
660
|
+
const kt = this.constructor.keyType;
|
|
661
|
+
if (!this.constructor.incrementing && (kt === 'string' || kt === 'uuid' || kt === 'ulid') && !this.getKey()) {
|
|
662
|
+
this.setAttribute(this.constructor.primaryKey, this.constructor._generateKey());
|
|
537
663
|
}
|
|
538
664
|
|
|
539
665
|
const qb = this.constructor.query();
|
|
@@ -704,6 +830,27 @@ class Model {
|
|
|
704
830
|
return this.getKey() === other.getKey();
|
|
705
831
|
}
|
|
706
832
|
|
|
833
|
+
isNot(other) {
|
|
834
|
+
return !this.is(other);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
replicate(except = []) {
|
|
838
|
+
const exclude = new Set([
|
|
839
|
+
this.constructor.primaryKey,
|
|
840
|
+
...(this.constructor.timestamps ? ['created_at', 'updated_at'] : []),
|
|
841
|
+
...except,
|
|
842
|
+
]);
|
|
843
|
+
const attrs = {};
|
|
844
|
+
for (const [k, v] of Object.entries(this.attributes)) {
|
|
845
|
+
if (!exclude.has(k)) attrs[k] = v;
|
|
846
|
+
}
|
|
847
|
+
const copy = new this.constructor(attrs);
|
|
848
|
+
copy._initialize();
|
|
849
|
+
copy.exists = false;
|
|
850
|
+
copy.wasRecentlyCreated = false;
|
|
851
|
+
return copy;
|
|
852
|
+
}
|
|
853
|
+
|
|
707
854
|
// JSON serialization
|
|
708
855
|
toJSON() {
|
|
709
856
|
this._initialize();
|
|
@@ -799,4 +946,21 @@ class Model {
|
|
|
799
946
|
}
|
|
800
947
|
}
|
|
801
948
|
|
|
802
|
-
|
|
949
|
+
// Any QueryBuilder instance method not already forwarded above (e.g. where(),
|
|
950
|
+
// orderBy(), whereIn(), paginate()...) is auto-forwarded from Model.<method>()
|
|
951
|
+
// to Model.query().<method>(), so new QueryBuilder methods don't need a
|
|
952
|
+
// matching static added here to be callable directly on a Model subclass.
|
|
953
|
+
const ModelStaticHandler = {
|
|
954
|
+
get(target, prop, receiver) {
|
|
955
|
+
if (typeof prop === 'symbol' || prop in target) {
|
|
956
|
+
return Reflect.get(target, prop, receiver);
|
|
957
|
+
}
|
|
958
|
+
const qbMethod = QueryBuilder.prototype[prop];
|
|
959
|
+
if (typeof prop === 'string' && prop[0] !== '_' && typeof qbMethod === 'function') {
|
|
960
|
+
return (...args) => receiver.query()[prop](...args);
|
|
961
|
+
}
|
|
962
|
+
return Reflect.get(target, prop, receiver);
|
|
963
|
+
}
|
|
964
|
+
};
|
|
965
|
+
|
|
966
|
+
module.exports = new Proxy(Model, ModelStaticHandler);
|
package/orm/QueryBuilder.d.ts
CHANGED
|
@@ -104,12 +104,21 @@ export default class QueryBuilder {
|
|
|
104
104
|
// Selection
|
|
105
105
|
select(...columns: any[]): this;
|
|
106
106
|
addSelect(...columns: any[]): this;
|
|
107
|
+
addSelect(subqueries: { [alias: string]: (query: QueryBuilder) => void }): this;
|
|
107
108
|
distinct(): this;
|
|
108
109
|
|
|
109
110
|
// Raw queries
|
|
110
111
|
whereRaw(sql: string, bindings?: any[]): this;
|
|
111
112
|
selectRaw(sql: string, bindings?: any[]): this;
|
|
112
113
|
|
|
114
|
+
// Advanced subqueries
|
|
115
|
+
orderBySubquery(callback: (query: QueryBuilder) => void, direction?: 'asc' | 'desc'): this;
|
|
116
|
+
|
|
117
|
+
// Pending attributes (scope defaults)
|
|
118
|
+
withPendingAttributes(attributes: { [key: string]: any }): this;
|
|
119
|
+
new(attributes?: { [key: string]: any }): Promise<import('./Model').default>;
|
|
120
|
+
create(attributes?: { [key: string]: any }): Promise<import('./Model').default>;
|
|
121
|
+
|
|
113
122
|
// Aggregates
|
|
114
123
|
count(column?: string): Promise<number>;
|
|
115
124
|
sum(column: string): Promise<number>;
|
|
@@ -125,6 +134,7 @@ export default class QueryBuilder {
|
|
|
125
134
|
whereHas(relation: string, callback?: (query: QueryBuilder) => void): this;
|
|
126
135
|
doesntHave(relation: string): this;
|
|
127
136
|
whereDoesntHave(relation: string, callback?: (query: QueryBuilder) => void): this;
|
|
137
|
+
has(relation: string, operator?: '=' | '!=' | '<' | '<=' | '>' | '>=', count?: number): this;
|
|
128
138
|
|
|
129
139
|
// Execution methods
|
|
130
140
|
get(): Promise<Collection<Model>>;
|