ilana-orm 1.0.17 → 1.0.19
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 +293 -37
- package/cli/ilana.js +60 -158
- package/database/connection.d.ts +3 -0
- package/database/connection.js +40 -1
- package/database/schema-builder.js +4 -0
- package/ilana.config.js +1 -1
- package/index.d.ts +17 -0
- package/index.edge.js +10 -0
- package/index.edge.mjs +37 -0
- package/index.js +6 -2
- package/index.mjs +2 -0
- package/jest.config.js +4 -0
- package/orm/Errors.js +18 -0
- package/orm/F.js +22 -0
- package/orm/Factory.js +49 -12
- package/orm/MigrationRunner.js +16 -48
- package/orm/Model.d.ts +25 -1
- package/orm/Model.js +219 -19
- package/orm/QueryBuilder.d.ts +18 -0
- package/orm/QueryBuilder.js +109 -3
- package/package.json +6 -1
package/orm/Factory.js
CHANGED
|
@@ -14,6 +14,9 @@ class Factory {
|
|
|
14
14
|
this.count = 1;
|
|
15
15
|
this.currentStates = [];
|
|
16
16
|
this.relationships = new Map();
|
|
17
|
+
this._has = new Map();
|
|
18
|
+
this._for = new Map();
|
|
19
|
+
this._hasAttached = new Map();
|
|
17
20
|
this._sequenceCount = 0;
|
|
18
21
|
this.sequences = new Map();
|
|
19
22
|
}
|
|
@@ -57,23 +60,30 @@ class Factory {
|
|
|
57
60
|
return this;
|
|
58
61
|
}
|
|
59
62
|
|
|
60
|
-
for(relation,
|
|
61
|
-
this.
|
|
63
|
+
for(relation, relFactory) {
|
|
64
|
+
this._for.set(relation, relFactory);
|
|
62
65
|
return this;
|
|
63
66
|
}
|
|
64
67
|
|
|
65
|
-
has(
|
|
66
|
-
|
|
67
|
-
this.relationships.set(relation, factory);
|
|
68
|
-
}
|
|
68
|
+
has(relFactory, relation) {
|
|
69
|
+
this._has.set(relation, relFactory);
|
|
69
70
|
return this;
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
hasAttached(
|
|
73
|
-
this.
|
|
73
|
+
hasAttached(relFactory, relation) {
|
|
74
|
+
this._hasAttached.set(relation, relFactory);
|
|
74
75
|
return this;
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
_getRelation(relationName) {
|
|
79
|
+
try {
|
|
80
|
+
const dummy = new this.model({});
|
|
81
|
+
return typeof dummy[relationName] === 'function' ? dummy[relationName]() : null;
|
|
82
|
+
} catch (_) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
77
87
|
sequence() {
|
|
78
88
|
return ++this._sequenceCount;
|
|
79
89
|
}
|
|
@@ -173,16 +183,40 @@ class Factory {
|
|
|
173
183
|
}
|
|
174
184
|
|
|
175
185
|
async createOne(attributes = {}) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
186
|
+
// Handle 'for' (belongsTo) — create parent first, inject FK into this model
|
|
187
|
+
const parentAttrs = {};
|
|
188
|
+
for (const [relationName, relFactory] of this._for) {
|
|
189
|
+
const parent = await relFactory.createOne({});
|
|
190
|
+
const rel = this._getRelation(relationName);
|
|
191
|
+
if (rel && rel.foreignKey) parentAttrs[rel.foreignKey] = parent.getKey();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const model = await this.makeOne({ ...parentAttrs, ...attributes });
|
|
195
|
+
|
|
179
196
|
for (const callback of this._beforeCreatingCallbacks) {
|
|
180
197
|
await callback(model);
|
|
181
198
|
}
|
|
182
199
|
|
|
183
200
|
await model.save();
|
|
184
201
|
|
|
185
|
-
//
|
|
202
|
+
// Handle 'has' (hasMany) — create children with FK pointing to this model
|
|
203
|
+
for (const [relationName, relFactory] of this._has) {
|
|
204
|
+
const rel = this._getRelation(relationName);
|
|
205
|
+
if (rel && rel.foreignKey) {
|
|
206
|
+
await relFactory.create({ [rel.foreignKey]: model.getKey() });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Handle 'hasAttached' (belongsToMany) — create and attach via pivot
|
|
211
|
+
for (const [relationName, relFactory] of this._hasAttached) {
|
|
212
|
+
const related = await relFactory.create({});
|
|
213
|
+
const relatedArr = Array.isArray(related) ? related : [related];
|
|
214
|
+
const rel = this._getRelation(relationName);
|
|
215
|
+
if (rel && typeof rel.attach === 'function') {
|
|
216
|
+
for (const r of relatedArr) await rel.attach(r.getKey());
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
186
220
|
for (const callback of this._afterCreatingCallbacks) {
|
|
187
221
|
await callback(model);
|
|
188
222
|
}
|
|
@@ -354,6 +388,9 @@ if (typeof Model !== 'undefined') {
|
|
|
354
388
|
newFactory.states = new Map(existingFactory.states);
|
|
355
389
|
newFactory._afterCreatingCallbacks = [...existingFactory._afterCreatingCallbacks];
|
|
356
390
|
newFactory._beforeCreatingCallbacks = [...existingFactory._beforeCreatingCallbacks];
|
|
391
|
+
newFactory._has = new Map(existingFactory._has);
|
|
392
|
+
newFactory._for = new Map(existingFactory._for);
|
|
393
|
+
newFactory._hasAttached = new Map(existingFactory._hasAttached);
|
|
357
394
|
return newFactory;
|
|
358
395
|
}
|
|
359
396
|
throw new Error(`No factory defined for model: ${this.name}`);
|
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
|
@@ -36,7 +36,7 @@ export default class Model<TAttributes extends ModelAttributes = ModelAttributes
|
|
|
36
36
|
protected static table: string;
|
|
37
37
|
protected static connection?: string;
|
|
38
38
|
protected static primaryKey: string;
|
|
39
|
-
protected static keyType: 'number' | 'string';
|
|
39
|
+
protected static keyType: 'number' | 'string' | 'uuid' | 'ulid';
|
|
40
40
|
protected static incrementing: boolean;
|
|
41
41
|
protected static timestamps: boolean;
|
|
42
42
|
protected static createdAt: string;
|
|
@@ -50,6 +50,12 @@ export default class Model<TAttributes extends ModelAttributes = ModelAttributes
|
|
|
50
50
|
protected static globalScopes: Map<string, (query: QueryBuilder) => void>;
|
|
51
51
|
protected static appends: string[];
|
|
52
52
|
protected static timezone: string;
|
|
53
|
+
static strictLoading: boolean;
|
|
54
|
+
static touches: string[];
|
|
55
|
+
static enums: { [column: string]: string[] };
|
|
56
|
+
static embeddingColumn: string;
|
|
57
|
+
static embeddingDimensions: number;
|
|
58
|
+
static embeddingProvider?: (text: string) => Promise<number[]>;
|
|
53
59
|
|
|
54
60
|
// Instance properties
|
|
55
61
|
attributes: ModelAttributes;
|
|
@@ -83,13 +89,25 @@ export default class Model<TAttributes extends ModelAttributes = ModelAttributes
|
|
|
83
89
|
static oldest(column?: string): QueryBuilder;
|
|
84
90
|
static withTrashed(): QueryBuilder;
|
|
85
91
|
static onlyTrashed(): QueryBuilder;
|
|
92
|
+
static withoutTrashed(): QueryBuilder;
|
|
93
|
+
static findOrFail(id: number | string): Promise<Model>;
|
|
94
|
+
static insertGetId(data: { [key: string]: any }): Promise<number | string>;
|
|
86
95
|
static upsert(data: any[], uniqueBy: string[], update?: string[]): Promise<any>;
|
|
87
96
|
static withoutGlobalScopes(): QueryBuilder;
|
|
88
97
|
static make(attributes?: ModelAttributes): Model;
|
|
89
98
|
static create(attributes?: ModelAttributes): Promise<Model>;
|
|
90
99
|
static generateUuid(): string;
|
|
100
|
+
static generateUlid(): string;
|
|
101
|
+
static _generateKey(): string;
|
|
102
|
+
static withoutEvents<T>(callback: () => Promise<T>): Promise<T>;
|
|
103
|
+
static prunable(): QueryBuilder;
|
|
104
|
+
static prune(): Promise<number>;
|
|
91
105
|
static insert(data: ModelAttributes | ModelAttributes[]): Promise<any>;
|
|
92
106
|
static destroy(ids: any | any[]): Promise<number>;
|
|
107
|
+
static truncate(): Promise<void>;
|
|
108
|
+
static seed(count?: number): Promise<any[]>;
|
|
109
|
+
static nearestTo(vector: number[], options?: { limit?: number; column?: string; distance?: 'cosine' | 'l2' | 'inner' }): Promise<Collection<any>>;
|
|
110
|
+
static search(text: string, options?: { limit?: number; column?: string; distance?: 'cosine' | 'l2' | 'inner'; provider?: (text: string) => Promise<number[]> }): Promise<Collection<any>>;
|
|
93
111
|
static firstOrCreate(attributes: ModelAttributes, values?: ModelAttributes): Promise<Model>;
|
|
94
112
|
static firstOrNew(attributes: ModelAttributes, values?: ModelAttributes): Promise<Model>;
|
|
95
113
|
static updateOrCreate(attributes: ModelAttributes, values?: ModelAttributes): Promise<Model>;
|
|
@@ -143,10 +161,16 @@ export default class Model<TAttributes extends ModelAttributes = ModelAttributes
|
|
|
143
161
|
getDirty(): ModelAttributes;
|
|
144
162
|
delete(): Promise<boolean>;
|
|
145
163
|
restore(): Promise<boolean>;
|
|
164
|
+
increment(column: string, amount?: number): Promise<this>;
|
|
165
|
+
decrement(column: string, amount?: number): Promise<this>;
|
|
146
166
|
trashed(): boolean;
|
|
147
167
|
only(keys: string[]): ModelAttributes;
|
|
148
168
|
except(keys: string[]): ModelAttributes;
|
|
149
169
|
forceDelete(): Promise<boolean>;
|
|
170
|
+
fresh(): Promise<this | null>;
|
|
171
|
+
is(other: Model): boolean;
|
|
172
|
+
isNot(other: Model | null | undefined): boolean;
|
|
173
|
+
replicate(except?: string[]): this;
|
|
150
174
|
toJSON(): any;
|
|
151
175
|
|
|
152
176
|
// Relationships
|
package/orm/Model.js
CHANGED
|
@@ -4,20 +4,22 @@ const { HasOne, HasMany, BelongsTo, BelongsToMany, HasManyThrough, MorphTo, Morp
|
|
|
4
4
|
const ModelRegistry = require('./ModelRegistry');
|
|
5
5
|
const Database = require('../database/connection');
|
|
6
6
|
|
|
7
|
-
// Auto-load configuration on first import
|
|
8
|
-
(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
7
|
+
// Auto-load configuration on first import (skipped in edge runtime)
|
|
8
|
+
if (typeof process !== 'undefined' && process.versions && process.versions.node && !global.__ILANA_EDGE__) {
|
|
9
|
+
(function autoLoadConfig() {
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const configPathJs = path.join(process.cwd(), 'ilana.config.js');
|
|
13
|
+
const configPathMjs = path.join(process.cwd(), 'ilana.config.mjs');
|
|
14
|
+
|
|
15
|
+
if (fs.existsSync(configPathJs)) {
|
|
16
|
+
delete require.cache[configPathJs];
|
|
17
|
+
require(configPathJs);
|
|
18
|
+
} else if (fs.existsSync(configPathMjs)) {
|
|
19
|
+
// For ES modules, handled in _getConfig
|
|
20
|
+
}
|
|
21
|
+
})();
|
|
22
|
+
}
|
|
21
23
|
|
|
22
24
|
class Model {
|
|
23
25
|
// --- Static defaults ---
|
|
@@ -35,6 +37,11 @@ class Model {
|
|
|
35
37
|
static globalScopes = new Map();
|
|
36
38
|
static appends = [];
|
|
37
39
|
static timezone = 'UTC';
|
|
40
|
+
static strictLoading = false;
|
|
41
|
+
static touches = [];
|
|
42
|
+
static enums = {};
|
|
43
|
+
static embeddingColumn = 'embedding';
|
|
44
|
+
static embeddingDimensions = 1536;
|
|
38
45
|
|
|
39
46
|
// --- Instance props ---
|
|
40
47
|
attributes = {};
|
|
@@ -62,6 +69,25 @@ class Model {
|
|
|
62
69
|
? this.appends
|
|
63
70
|
: this.constructor.appends;
|
|
64
71
|
|
|
72
|
+
// Wrap relations in a Proxy for strict loading enforcement
|
|
73
|
+
const modelClass = this.constructor;
|
|
74
|
+
this.relations = new Proxy({}, {
|
|
75
|
+
get(target, key) {
|
|
76
|
+
if (typeof key !== 'string') return target[key];
|
|
77
|
+
if (modelClass.strictLoading && !(key in target)) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`Strict loading violation: '${key}' was not eager loaded on ${modelClass.name}. ` +
|
|
80
|
+
`Use .with('${key}') in your query.`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return target[key];
|
|
84
|
+
},
|
|
85
|
+
set(target, key, value) { target[key] = value; return true; },
|
|
86
|
+
has(target, key) { return key in target; },
|
|
87
|
+
ownKeys(target) { return Object.keys(target); },
|
|
88
|
+
getOwnPropertyDescriptor(target, key) { return Object.getOwnPropertyDescriptor(target, key); },
|
|
89
|
+
});
|
|
90
|
+
|
|
65
91
|
// defer attribute setting
|
|
66
92
|
if (attrs && Object.keys(attrs).length) this._deferred = attrs;
|
|
67
93
|
|
|
@@ -79,6 +105,7 @@ class Model {
|
|
|
79
105
|
this._deferred = null;
|
|
80
106
|
// Recreate getters after initialization
|
|
81
107
|
this._createAttributeGetters();
|
|
108
|
+
this._generateEnumHelpers();
|
|
82
109
|
}
|
|
83
110
|
|
|
84
111
|
_createAttributeGetters() {
|
|
@@ -106,6 +133,26 @@ class Model {
|
|
|
106
133
|
}
|
|
107
134
|
}
|
|
108
135
|
|
|
136
|
+
_generateEnumHelpers() {
|
|
137
|
+
const enums = this.constructor.enums || {};
|
|
138
|
+
for (const [column, values] of Object.entries(enums)) {
|
|
139
|
+
for (const value of values) {
|
|
140
|
+
const pascal = value.charAt(0).toUpperCase() + value.slice(1);
|
|
141
|
+
const isMethod = `is${pascal}`;
|
|
142
|
+
const makeMethod = `make${pascal}`;
|
|
143
|
+
if (!this[isMethod]) {
|
|
144
|
+
this[isMethod] = () => this.getAttribute(column) === value;
|
|
145
|
+
}
|
|
146
|
+
if (!this[makeMethod]) {
|
|
147
|
+
this[makeMethod] = async () => {
|
|
148
|
+
this.setAttribute(column, value);
|
|
149
|
+
return this.save();
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
109
156
|
_toPascalCase(key) {
|
|
110
157
|
return key.replace(/(^|_)([a-z])/g, (_, __, c) => c.toUpperCase());
|
|
111
158
|
}
|
|
@@ -182,6 +229,9 @@ class Model {
|
|
|
182
229
|
static oldest(col) { return this.query().oldest(col || this.createdAt || 'created_at'); }
|
|
183
230
|
static withTrashed() { return this.query().withTrashed(); }
|
|
184
231
|
static onlyTrashed() { return this.query().onlyTrashed(); }
|
|
232
|
+
static withoutTrashed() { return this.query().withoutTrashed(); }
|
|
233
|
+
static async findOrFail(id) { return this.query().findOrFail(id); }
|
|
234
|
+
static async insertGetId(data) { return this.query().insertGetId(data); }
|
|
185
235
|
static async upsert(data, uniqueBy, update) { return this.query().upsert(data, uniqueBy, update); }
|
|
186
236
|
static withoutGlobalScopes() {
|
|
187
237
|
return new QueryBuilder(this.getTableName(), this, this.getConnectionName());
|
|
@@ -190,8 +240,8 @@ class Model {
|
|
|
190
240
|
static make(attrs = {}) {
|
|
191
241
|
const inst = new this(attrs);
|
|
192
242
|
inst._initialize();
|
|
193
|
-
if (!this.incrementing && this.keyType === 'string' && !inst.getKey()) {
|
|
194
|
-
inst.setAttribute(this.primaryKey, this.
|
|
243
|
+
if (!this.incrementing && (this.keyType === 'string' || this.keyType === 'uuid' || this.keyType === 'ulid') && !inst.getKey()) {
|
|
244
|
+
inst.setAttribute(this.primaryKey, this._generateKey());
|
|
195
245
|
}
|
|
196
246
|
return inst;
|
|
197
247
|
}
|
|
@@ -209,7 +259,67 @@ class Model {
|
|
|
209
259
|
});
|
|
210
260
|
}
|
|
211
261
|
|
|
262
|
+
static generateUlid() {
|
|
263
|
+
const CHARS = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
264
|
+
const now = Date.now();
|
|
265
|
+
let t = now;
|
|
266
|
+
let ts = '';
|
|
267
|
+
for (let i = 9; i >= 0; i--) {
|
|
268
|
+
ts = CHARS[t % 32] + ts;
|
|
269
|
+
t = Math.floor(t / 32);
|
|
270
|
+
}
|
|
271
|
+
let rand = '';
|
|
272
|
+
for (let i = 0; i < 16; i++) rand += CHARS[Math.floor(Math.random() * 32)];
|
|
273
|
+
return ts + rand;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
static _generateKey() {
|
|
277
|
+
if (this.keyType === 'ulid') return this.generateUlid();
|
|
278
|
+
return this.generateUuid();
|
|
279
|
+
}
|
|
280
|
+
|
|
212
281
|
static async insert(data) { return this.query().insert(data); }
|
|
282
|
+
static async truncate() { return this.query().toKnex().truncate(); }
|
|
283
|
+
|
|
284
|
+
static _assertPgVector() {
|
|
285
|
+
const conn = Database.connection(this.connection);
|
|
286
|
+
const client = conn?.client?.config?.client || '';
|
|
287
|
+
if (!client.includes('pg')) {
|
|
288
|
+
throw new Error(
|
|
289
|
+
`${this.name}.search() and ${this.name}.nearestTo() require PostgreSQL with the pgvector extension. ` +
|
|
290
|
+
`Current database client is '${client || 'unknown'}'. ` +
|
|
291
|
+
`Vector search is not supported on MySQL or SQLite.`
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
static async nearestTo(vector, { limit = 10, column, distance = 'cosine' } = {}) {
|
|
297
|
+
this._assertPgVector();
|
|
298
|
+
const col = column || this.embeddingColumn;
|
|
299
|
+
const ops = { cosine: '<=>', l2: '<->', inner: '<#>' };
|
|
300
|
+
const op = ops[distance] || '<=>';
|
|
301
|
+
const vectorStr = `[${Array.from(vector).join(',')}]`;
|
|
302
|
+
return this.query()
|
|
303
|
+
.selectRaw(`*, (${col} ${op} ?) as distance`, [vectorStr])
|
|
304
|
+
.orderByRaw(`${col} ${op} ?`, [vectorStr])
|
|
305
|
+
.limit(limit)
|
|
306
|
+
.get();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
static async search(text, { limit = 10, column, distance = 'cosine', provider } = {}) {
|
|
310
|
+
this._assertPgVector();
|
|
311
|
+
const embed = provider || this.embeddingProvider;
|
|
312
|
+
if (!embed) throw new Error(
|
|
313
|
+
`${this.name}.search() requires an embedding provider. ` +
|
|
314
|
+
`Pass { provider: async (text) => number[] } or set ${this.name}.embeddingProvider.`
|
|
315
|
+
);
|
|
316
|
+
const vector = await embed(text);
|
|
317
|
+
return this.nearestTo(vector, { limit, column, distance });
|
|
318
|
+
}
|
|
319
|
+
static async seed(count = 1) {
|
|
320
|
+
const { factory } = require('./Factory');
|
|
321
|
+
return factory(this).times(count).create();
|
|
322
|
+
}
|
|
213
323
|
static async destroy(ids) {
|
|
214
324
|
const idList = Array.isArray(ids) ? ids : [ids];
|
|
215
325
|
if (this.softDeletes) {
|
|
@@ -293,6 +403,7 @@ class Model {
|
|
|
293
403
|
}
|
|
294
404
|
// static async fireEvent(evt, mdl) { for (const h of this.events[evt] || []) if (await h(mdl) === false) return false; }
|
|
295
405
|
static async fireEvent(evt, mdl) {
|
|
406
|
+
if (this._mutingEvents) return true;
|
|
296
407
|
const ownEvents = Object.hasOwn(this, 'events') ? this.events : {};
|
|
297
408
|
const handlers = ownEvents[evt] || [];
|
|
298
409
|
for (const handler of handlers) {
|
|
@@ -301,6 +412,31 @@ class Model {
|
|
|
301
412
|
return true;
|
|
302
413
|
}
|
|
303
414
|
|
|
415
|
+
static async withoutEvents(callback) {
|
|
416
|
+
this._mutingEvents = true;
|
|
417
|
+
try {
|
|
418
|
+
return await callback();
|
|
419
|
+
} finally {
|
|
420
|
+
this._mutingEvents = false;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
static prunable() {
|
|
425
|
+
throw new Error(`${this.name} must implement a static prunable() method that returns a QueryBuilder.`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
static async prune() {
|
|
429
|
+
const query = this.prunable();
|
|
430
|
+
let pruned = 0;
|
|
431
|
+
await query.chunk(1000, async (models) => {
|
|
432
|
+
for (const model of models) {
|
|
433
|
+
await model.delete();
|
|
434
|
+
pruned++;
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
return pruned;
|
|
438
|
+
}
|
|
439
|
+
|
|
304
440
|
// --- Instance methods ---
|
|
305
441
|
static getTableName() { return this.table || this.name.toLowerCase() + 's'; }
|
|
306
442
|
static getPrimaryKey() { return this.primaryKey; }
|
|
@@ -483,9 +619,10 @@ class Model {
|
|
|
483
619
|
this.setAttribute(createdAtCol, now).setAttribute(updatedAtCol, now);
|
|
484
620
|
}
|
|
485
621
|
|
|
486
|
-
// Generate UUID if needed
|
|
487
|
-
|
|
488
|
-
|
|
622
|
+
// Generate UUID/ULID if needed
|
|
623
|
+
const kt = this.constructor.keyType;
|
|
624
|
+
if (!this.constructor.incrementing && (kt === 'string' || kt === 'uuid' || kt === 'ulid') && !this.getKey()) {
|
|
625
|
+
this.setAttribute(this.constructor.primaryKey, this.constructor._generateKey());
|
|
489
626
|
}
|
|
490
627
|
|
|
491
628
|
const qb = this.constructor.query();
|
|
@@ -525,9 +662,27 @@ class Model {
|
|
|
525
662
|
this.syncOriginal();
|
|
526
663
|
}
|
|
527
664
|
|
|
665
|
+
await this._touchRelations();
|
|
666
|
+
|
|
528
667
|
return true;
|
|
529
668
|
}
|
|
530
669
|
|
|
670
|
+
async _touchRelations() {
|
|
671
|
+
const touches = this.constructor.touches || [];
|
|
672
|
+
for (const relName of touches) {
|
|
673
|
+
if (typeof this[relName] !== 'function') continue;
|
|
674
|
+
const rel = this[relName]();
|
|
675
|
+
if (!rel || rel.constructor.name !== 'BelongsTo') continue;
|
|
676
|
+
const parentClass = rel.getRelatedClass();
|
|
677
|
+
const parentId = this.getAttribute(rel.foreignKey);
|
|
678
|
+
if (!parentId) continue;
|
|
679
|
+
const col = parentClass.updatedAt || 'updated_at';
|
|
680
|
+
await parentClass.query()
|
|
681
|
+
.where(parentClass.primaryKey || 'id', parentId)
|
|
682
|
+
.update({ [col]: new Date() });
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
531
686
|
async update(attributes = {}) {
|
|
532
687
|
this.fill(attributes);
|
|
533
688
|
return await this.save();
|
|
@@ -604,6 +759,20 @@ class Model {
|
|
|
604
759
|
return result;
|
|
605
760
|
}
|
|
606
761
|
|
|
762
|
+
async increment(column, amount = 1) {
|
|
763
|
+
await this.constructor.query().where(this.constructor.primaryKey, this.getKey()).increment(column, amount);
|
|
764
|
+
this.setAttribute(column, (this.getAttribute(column) || 0) + amount);
|
|
765
|
+
this.syncOriginal();
|
|
766
|
+
return this;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
async decrement(column, amount = 1) {
|
|
770
|
+
await this.constructor.query().where(this.constructor.primaryKey, this.getKey()).decrement(column, amount);
|
|
771
|
+
this.setAttribute(column, (this.getAttribute(column) || 0) - amount);
|
|
772
|
+
this.syncOriginal();
|
|
773
|
+
return this;
|
|
774
|
+
}
|
|
775
|
+
|
|
607
776
|
async forceDelete() {
|
|
608
777
|
if (!this.exists) return false;
|
|
609
778
|
|
|
@@ -614,6 +783,37 @@ class Model {
|
|
|
614
783
|
return true;
|
|
615
784
|
}
|
|
616
785
|
|
|
786
|
+
async fresh() {
|
|
787
|
+
if (!this.exists) return null;
|
|
788
|
+
return this.constructor.find(this.getKey());
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
is(other) {
|
|
792
|
+
if (!other || !(other instanceof this.constructor)) return false;
|
|
793
|
+
return this.getKey() === other.getKey();
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
isNot(other) {
|
|
797
|
+
return !this.is(other);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
replicate(except = []) {
|
|
801
|
+
const exclude = new Set([
|
|
802
|
+
this.constructor.primaryKey,
|
|
803
|
+
...(this.constructor.timestamps ? ['created_at', 'updated_at'] : []),
|
|
804
|
+
...except,
|
|
805
|
+
]);
|
|
806
|
+
const attrs = {};
|
|
807
|
+
for (const [k, v] of Object.entries(this.attributes)) {
|
|
808
|
+
if (!exclude.has(k)) attrs[k] = v;
|
|
809
|
+
}
|
|
810
|
+
const copy = new this.constructor(attrs);
|
|
811
|
+
copy._initialize();
|
|
812
|
+
copy.exists = false;
|
|
813
|
+
copy.wasRecentlyCreated = false;
|
|
814
|
+
return copy;
|
|
815
|
+
}
|
|
816
|
+
|
|
617
817
|
// JSON serialization
|
|
618
818
|
toJSON() {
|
|
619
819
|
this._initialize();
|
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>>;
|
|
@@ -135,6 +145,8 @@ export default class QueryBuilder {
|
|
|
135
145
|
pluck(column: string): Promise<any[]>;
|
|
136
146
|
exists(): Promise<boolean>;
|
|
137
147
|
doesntExist(): Promise<boolean>;
|
|
148
|
+
sole(): Promise<Model>;
|
|
149
|
+
tap(callback: (query: this) => void): this;
|
|
138
150
|
|
|
139
151
|
// Pagination
|
|
140
152
|
paginate(page?: number, perPage?: number): Promise<PaginationResult<Model>>;
|
|
@@ -150,6 +162,8 @@ export default class QueryBuilder {
|
|
|
150
162
|
insert(data: any | any[]): Promise<any>;
|
|
151
163
|
insertGetId(data: any): Promise<any>;
|
|
152
164
|
update(data: any): Promise<number>;
|
|
165
|
+
increment(column: string, amount?: number): Promise<number>;
|
|
166
|
+
decrement(column: string, amount?: number): Promise<number>;
|
|
153
167
|
delete(): Promise<number>;
|
|
154
168
|
upsert(data: any[], uniqueBy: string[], update?: string[]): Promise<any>;
|
|
155
169
|
|
|
@@ -167,6 +181,10 @@ export default class QueryBuilder {
|
|
|
167
181
|
withTrashed(): this;
|
|
168
182
|
onlyTrashed(): this;
|
|
169
183
|
withoutTrashed(): this;
|
|
184
|
+
restore(): Promise<number>;
|
|
185
|
+
|
|
186
|
+
// Plain object result (no model hydration)
|
|
187
|
+
values(): Promise<any[]>;
|
|
170
188
|
|
|
171
189
|
// Debug
|
|
172
190
|
toSql(): string;
|