ilana-orm 1.0.18 → 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 +114 -31
- package/cli/ilana.js +31 -145
- package/database/schema-builder.js +4 -0
- package/index.edge.js +10 -0
- package/index.edge.mjs +37 -0
- package/orm/MigrationRunner.js +16 -48
- package/orm/Model.d.ts +16 -1
- package/orm/Model.js +129 -19
- package/orm/QueryBuilder.d.ts +10 -0
- package/orm/QueryBuilder.js +74 -1
- package/package.json +6 -1
package/README.md
CHANGED
|
@@ -24,6 +24,7 @@ A fully-featured, Eloquent-style ORM for Node.js with automatic TypeScript suppo
|
|
|
24
24
|
- [Schema Builder](#schema-builder)
|
|
25
25
|
- [Transactions](#transactions)
|
|
26
26
|
- [Advanced Features](#advanced-features)
|
|
27
|
+
- [Supabase](#supabase)
|
|
27
28
|
- [Complete API Reference](#complete-api-reference)
|
|
28
29
|
- [TypeScript Support](#typescript-support)
|
|
29
30
|
- [Performance & Best Practices](#performance--best-practices)
|
|
@@ -793,47 +794,33 @@ export default class User extends Model {
|
|
|
793
794
|
|
|
794
795
|
### UUID Primary Keys
|
|
795
796
|
|
|
796
|
-
**JavaScript:**
|
|
797
797
|
```javascript
|
|
798
798
|
class User extends Model {
|
|
799
799
|
static table = 'users';
|
|
800
|
-
static keyType = '
|
|
800
|
+
static keyType = 'uuid';
|
|
801
801
|
static incrementing = false;
|
|
802
802
|
}
|
|
803
803
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
const user = await User.create({
|
|
808
|
-
name: 'John Doe',
|
|
809
|
-
email: 'john@example.com',
|
|
810
|
-
});
|
|
811
|
-
// user.id will be a generated UUID
|
|
812
|
-
````
|
|
804
|
+
const user = await User.create({ name: 'John Doe', email: 'john@example.com' });
|
|
805
|
+
// user.id → "550e8400-e29b-41d4-a716-446655440000"
|
|
806
|
+
```
|
|
813
807
|
|
|
814
|
-
|
|
808
|
+
### ULID Primary Keys
|
|
815
809
|
|
|
816
|
-
|
|
817
|
-
export default class User extends Model {
|
|
818
|
-
protected static table = "users";
|
|
819
|
-
protected static keyType = "string" as const;
|
|
820
|
-
protected static incrementing = false;
|
|
810
|
+
ULIDs are 26-character sortable identifiers — URL-safe, lexicographically ordered by creation time:
|
|
821
811
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
812
|
+
```javascript
|
|
813
|
+
class Order extends Model {
|
|
814
|
+
static table = 'orders';
|
|
815
|
+
static keyType = 'ulid';
|
|
816
|
+
static incrementing = false;
|
|
826
817
|
}
|
|
827
818
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
name: "John Doe",
|
|
831
|
-
email: "john@example.com",
|
|
832
|
-
});
|
|
833
|
-
// user.id will be a generated UUID
|
|
819
|
+
const order = await Order.create({ total: 49.99 });
|
|
820
|
+
// order.id → "01J3X7KQZB8YTPNMCHW4RSVFGE"
|
|
834
821
|
```
|
|
835
822
|
|
|
836
|
-
|
|
823
|
+
Use `char(26)` for the column type in migrations.
|
|
837
824
|
|
|
838
825
|
### Attribute Casting
|
|
839
826
|
|
|
@@ -4179,6 +4166,68 @@ const users = await User.query().select('id', 'name', 'email').values();
|
|
|
4179
4166
|
// [{ id: 1, name: 'John', email: 'john@example.com' }, ...]
|
|
4180
4167
|
```
|
|
4181
4168
|
|
|
4169
|
+
### Vector / Semantic Search (pgvector)
|
|
4170
|
+
|
|
4171
|
+
Semantic search matches by **meaning**, not exact keywords. It works by converting text into a list of numbers called an **embedding** using an AI embedding model, storing those numbers in the database, and finding records whose embeddings are mathematically closest to a search query.
|
|
4172
|
+
|
|
4173
|
+
**Requires:** PostgreSQL + pgvector extension. An external embedding model (OpenAI, Cohere, Ollama, etc.) is also required — IlanaORM does not include one. Does **not** work with MySQL or SQLite.
|
|
4174
|
+
|
|
4175
|
+
```javascript
|
|
4176
|
+
// 1. Enable the extension in a migration
|
|
4177
|
+
await schema.enableVectorExtension();
|
|
4178
|
+
|
|
4179
|
+
// 2. Add a vector column
|
|
4180
|
+
await schema.table('posts', table => {
|
|
4181
|
+
table.specificType('embedding', 'vector(1536)'); // dimensions must match your model
|
|
4182
|
+
});
|
|
4183
|
+
|
|
4184
|
+
// 3. Configure the model
|
|
4185
|
+
class Post extends Model {
|
|
4186
|
+
static embeddingColumn = 'embedding';
|
|
4187
|
+
static embeddingProvider = async (text) => {
|
|
4188
|
+
// Must return Promise<number[]> — plug in any embedding API
|
|
4189
|
+
const res = await openai.embeddings.create({ model: 'text-embedding-ada-002', input: text });
|
|
4190
|
+
return res.data[0].embedding;
|
|
4191
|
+
};
|
|
4192
|
+
}
|
|
4193
|
+
|
|
4194
|
+
// 4. Store embeddings when creating records
|
|
4195
|
+
await Post.create({
|
|
4196
|
+
title: 'JavaScript tips',
|
|
4197
|
+
body: '...',
|
|
4198
|
+
embedding: JSON.stringify(await Post.embeddingProvider('JavaScript tips ...')),
|
|
4199
|
+
});
|
|
4200
|
+
|
|
4201
|
+
// 5. Search — converts text to vector, queries by similarity
|
|
4202
|
+
const posts = await Post.search('javascript performance tips', { limit: 5 });
|
|
4203
|
+
|
|
4204
|
+
// Or search by a raw vector you already have
|
|
4205
|
+
const posts = await Post.nearestTo(myVector, { distance: 'cosine', limit: 10 });
|
|
4206
|
+
|
|
4207
|
+
// Each result has a .distance attribute (lower = more similar)
|
|
4208
|
+
posts.forEach(p => console.log(p.title, p.distance));
|
|
4209
|
+
```
|
|
4210
|
+
|
|
4211
|
+
Distance options: `'cosine'` (default, `<=>`), `'l2'` (`<->`), `'inner'` (`<#>`).
|
|
4212
|
+
|
|
4213
|
+
### Edge Runtime
|
|
4214
|
+
|
|
4215
|
+
Import from `ilana-orm/edge` to skip the Node.js `fs`/`path` auto-loader. Required for Cloudflare Workers, Deno, Bun, and Next.js edge routes:
|
|
4216
|
+
|
|
4217
|
+
```javascript
|
|
4218
|
+
import { Model, Database } from 'ilana-orm/edge';
|
|
4219
|
+
|
|
4220
|
+
// Must configure explicitly — no auto-loading of ilana.config.js
|
|
4221
|
+
Database.configure({
|
|
4222
|
+
default: 'pg',
|
|
4223
|
+
connections: {
|
|
4224
|
+
pg: { client: 'pg', connection: { connectionString: process.env.DATABASE_URL } },
|
|
4225
|
+
},
|
|
4226
|
+
});
|
|
4227
|
+
|
|
4228
|
+
const users = await User.all();
|
|
4229
|
+
```
|
|
4230
|
+
|
|
4182
4231
|
## TypeScript Support
|
|
4183
4232
|
|
|
4184
4233
|
### Type-Safe Models
|
|
@@ -4231,6 +4280,32 @@ const user = await User.factory().create({
|
|
|
4231
4280
|
});
|
|
4232
4281
|
```
|
|
4233
4282
|
|
|
4283
|
+
## Supabase
|
|
4284
|
+
|
|
4285
|
+
Supabase is a hosted PostgreSQL platform. IlanaORM works with it out of the box using the `pg` driver — no special setup required.
|
|
4286
|
+
|
|
4287
|
+
```javascript
|
|
4288
|
+
// ilana.config.js
|
|
4289
|
+
const { Database } = require('ilana-orm');
|
|
4290
|
+
|
|
4291
|
+
Database.configure({
|
|
4292
|
+
default: 'pg',
|
|
4293
|
+
connections: {
|
|
4294
|
+
pg: {
|
|
4295
|
+
client: 'pg',
|
|
4296
|
+
connection: {
|
|
4297
|
+
connectionString: process.env.DATABASE_URL, // from Supabase dashboard
|
|
4298
|
+
ssl: { rejectUnauthorized: false }, // required for Supabase
|
|
4299
|
+
},
|
|
4300
|
+
},
|
|
4301
|
+
},
|
|
4302
|
+
});
|
|
4303
|
+
```
|
|
4304
|
+
|
|
4305
|
+
All features work including migrations, relations, soft deletes, and vector search (Supabase has pgvector built in).
|
|
4306
|
+
|
|
4307
|
+
For serverless or edge deployments with Supabase, use the [connection pooler URL](https://supabase.com/docs/guides/database/connecting-to-postgres) (port `6543`) and the `ilana-orm/edge` entry point for Cloudflare Workers or Next.js edge routes.
|
|
4308
|
+
|
|
4234
4309
|
## Performance & Best Practices
|
|
4235
4310
|
|
|
4236
4311
|
### Query Optimization
|
|
@@ -4410,10 +4485,14 @@ User.upsert(data, unique, update); // Upsert records
|
|
|
4410
4485
|
User.destroy(ids); // Delete by IDs (soft-delete aware)
|
|
4411
4486
|
User.truncate(); // Delete all rows in the table
|
|
4412
4487
|
User.seed(n); // Create n records using the registered factory
|
|
4488
|
+
User.prune(); // Delete all records matching static prunable()
|
|
4413
4489
|
User.withTrashed(); // Include soft deleted
|
|
4414
4490
|
User.onlyTrashed(); // Only soft deleted
|
|
4415
4491
|
User.withoutTrashed(); // Exclude soft deleted
|
|
4416
4492
|
|
|
4493
|
+
// Events
|
|
4494
|
+
User.withoutEvents(async () => { ... }); // Run without firing events
|
|
4495
|
+
|
|
4417
4496
|
// Configuration
|
|
4418
4497
|
User.getTableName(); // Get table name
|
|
4419
4498
|
User.getPrimaryKey(); // Get primary key
|
|
@@ -4421,6 +4500,7 @@ User.getKeyType(); // Get key type
|
|
|
4421
4500
|
User.getIncrementing(); // Get incrementing flag
|
|
4422
4501
|
User.getConnectionName(); // Get connection name
|
|
4423
4502
|
User.generateUuid(); // Generate UUID
|
|
4503
|
+
User.generateUlid(); // Generate ULID
|
|
4424
4504
|
|
|
4425
4505
|
// Events
|
|
4426
4506
|
User.creating(callback); // Before creating
|
|
@@ -4458,6 +4538,8 @@ user.forceDelete(); // Force delete (ignores softDeletes)
|
|
|
4458
4538
|
user.restore(); // Restore soft deleted
|
|
4459
4539
|
user.fresh(); // Re-fetch from DB and return new instance
|
|
4460
4540
|
user.is(other); // Check if two instances are the same record
|
|
4541
|
+
user.isNot(other); // Inverse of is()
|
|
4542
|
+
user.replicate(except?); // Clone as unsaved record (excludes PK + timestamps)
|
|
4461
4543
|
|
|
4462
4544
|
// Attributes
|
|
4463
4545
|
user.fill(attributes); // Mass assign (respects fillable/guarded)
|
|
@@ -4642,9 +4724,10 @@ query.upsert(data, uniqueBy, update);
|
|
|
4642
4724
|
query.with(...relations);
|
|
4643
4725
|
query.withConstraints(relation, callback);
|
|
4644
4726
|
query.withCount(...relations); // adds relation_count subquery column per model
|
|
4645
|
-
query.
|
|
4646
|
-
query.
|
|
4647
|
-
query.
|
|
4727
|
+
query.has(relation, operator?, count?); // WHERE EXISTS, or count-based (e.g. has('posts', '>', 5))
|
|
4728
|
+
query.whereHas(relation, callback); // WHERE EXISTS with constraint subquery
|
|
4729
|
+
query.doesntHave(relation); // WHERE NOT EXISTS subquery
|
|
4730
|
+
query.whereDoesntHave(relation, callback); // WHERE NOT EXISTS with constraint
|
|
4648
4731
|
```
|
|
4649
4732
|
|
|
4650
4733
|
#### Locking
|
package/cli/ilana.js
CHANGED
|
@@ -283,90 +283,20 @@ function getModelTemplate(className, tableName) {
|
|
|
283
283
|
|
|
284
284
|
if (isTypeScriptProject()) {
|
|
285
285
|
return `import Model from 'ilana-orm/orm/Model';
|
|
286
|
-
// import { MoneyCast, EncryptedCast } from 'ilana-orm/orm/CustomCasts';
|
|
287
286
|
|
|
288
287
|
export default class ${className} extends Model {
|
|
289
288
|
protected static table = '${tableName}';
|
|
290
|
-
protected static
|
|
291
|
-
protected static softDeletes = false;
|
|
292
|
-
|
|
293
|
-
// For UUID primary keys, uncomment:
|
|
294
|
-
// protected static keyType = 'string' as const;
|
|
295
|
-
// protected static incrementing = false;
|
|
296
|
-
|
|
297
|
-
protected fillable: string[] = [];
|
|
298
|
-
protected hidden: string[] = [];
|
|
299
|
-
protected appends: string[] = [];
|
|
300
|
-
protected casts = {
|
|
301
|
-
// Basic casts
|
|
302
|
-
// is_active: 'boolean' as const,
|
|
303
|
-
// metadata: 'json' as const,
|
|
304
|
-
// tags: 'array' as const,
|
|
305
|
-
|
|
306
|
-
// Custom casts
|
|
307
|
-
// price: new MoneyCast(),
|
|
308
|
-
// secret: new EncryptedCast('your-key'),
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
// Define relationships here
|
|
312
|
-
// example() {
|
|
313
|
-
// return this.hasMany(RelatedModel, 'foreign_key');
|
|
314
|
-
// }
|
|
315
|
-
|
|
316
|
-
// Define scopes here
|
|
317
|
-
// static scopeActive(query: any) {
|
|
318
|
-
// query.where('is_active', true);
|
|
319
|
-
// }
|
|
320
|
-
|
|
321
|
-
// Register for polymorphic relationships
|
|
322
|
-
// static {
|
|
323
|
-
// this.register();
|
|
324
|
-
// }
|
|
289
|
+
protected static fillable: string[] = [];
|
|
325
290
|
}
|
|
326
291
|
`;
|
|
327
292
|
}
|
|
328
293
|
|
|
329
294
|
if (isESModule) {
|
|
330
295
|
return `import Model from 'ilana-orm/orm/Model';
|
|
331
|
-
// import { MoneyCast, EncryptedCast } from 'ilana-orm/orm/CustomCasts';
|
|
332
296
|
|
|
333
297
|
class ${className} extends Model {
|
|
334
298
|
static table = '${tableName}';
|
|
335
|
-
static
|
|
336
|
-
static softDeletes = false;
|
|
337
|
-
|
|
338
|
-
// For UUID primary keys, uncomment:
|
|
339
|
-
// static keyType = 'string';
|
|
340
|
-
// static incrementing = false;
|
|
341
|
-
|
|
342
|
-
fillable = [];
|
|
343
|
-
hidden = [];
|
|
344
|
-
appends = [];
|
|
345
|
-
casts = {
|
|
346
|
-
// Basic casts
|
|
347
|
-
// is_active: 'boolean',
|
|
348
|
-
// metadata: 'json',
|
|
349
|
-
// tags: 'array',
|
|
350
|
-
|
|
351
|
-
// Custom casts
|
|
352
|
-
// price: new MoneyCast(),
|
|
353
|
-
// secret: new EncryptedCast('your-key'),
|
|
354
|
-
};
|
|
355
|
-
|
|
356
|
-
// Define relationships here
|
|
357
|
-
// example() {
|
|
358
|
-
// return this.hasMany(RelatedModel, 'foreign_key');
|
|
359
|
-
// }
|
|
360
|
-
|
|
361
|
-
// Define scopes here
|
|
362
|
-
// static scopeActive(query) {
|
|
363
|
-
// query.where('is_active', true);
|
|
364
|
-
// }
|
|
365
|
-
|
|
366
|
-
// Register for polymorphic relationships
|
|
367
|
-
// static {
|
|
368
|
-
// this.register();
|
|
369
|
-
// }
|
|
299
|
+
static fillable = [];
|
|
370
300
|
}
|
|
371
301
|
|
|
372
302
|
export default ${className};
|
|
@@ -374,45 +304,10 @@ export default ${className};
|
|
|
374
304
|
}
|
|
375
305
|
|
|
376
306
|
return `const Model = require('ilana-orm/orm/Model');
|
|
377
|
-
// const { MoneyCast, EncryptedCast } = require('ilana-orm/orm/CustomCasts');
|
|
378
307
|
|
|
379
308
|
class ${className} extends Model {
|
|
380
309
|
static table = '${tableName}';
|
|
381
|
-
static
|
|
382
|
-
static softDeletes = false;
|
|
383
|
-
|
|
384
|
-
// For UUID primary keys, uncomment:
|
|
385
|
-
// static keyType = 'string';
|
|
386
|
-
// static incrementing = false;
|
|
387
|
-
|
|
388
|
-
fillable = [];
|
|
389
|
-
hidden = [];
|
|
390
|
-
appends = [];
|
|
391
|
-
casts = {
|
|
392
|
-
// Basic casts
|
|
393
|
-
// is_active: 'boolean',
|
|
394
|
-
// metadata: 'json',
|
|
395
|
-
// tags: 'array',
|
|
396
|
-
|
|
397
|
-
// Custom casts
|
|
398
|
-
// price: new MoneyCast(),
|
|
399
|
-
// secret: new EncryptedCast('your-key'),
|
|
400
|
-
};
|
|
401
|
-
|
|
402
|
-
// Define relationships here
|
|
403
|
-
// example() {
|
|
404
|
-
// return this.hasMany(RelatedModel, 'foreign_key');
|
|
405
|
-
// }
|
|
406
|
-
|
|
407
|
-
// Define scopes here
|
|
408
|
-
// static scopeActive(query) {
|
|
409
|
-
// query.where('is_active', true);
|
|
410
|
-
// }
|
|
411
|
-
|
|
412
|
-
// Register for polymorphic relationships
|
|
413
|
-
// static {
|
|
414
|
-
// this.register();
|
|
415
|
-
// }
|
|
310
|
+
static fillable = [];
|
|
416
311
|
}
|
|
417
312
|
|
|
418
313
|
module.exports = ${className};
|
|
@@ -427,11 +322,7 @@ function getPivotModelTemplate(className, tableName) {
|
|
|
427
322
|
|
|
428
323
|
export default class ${className} extends Model {
|
|
429
324
|
protected static table = '${tableName}';
|
|
430
|
-
protected static
|
|
431
|
-
|
|
432
|
-
protected fillable: string[] = [];
|
|
433
|
-
|
|
434
|
-
// Define pivot relationships here
|
|
325
|
+
protected static fillable: string[] = [];
|
|
435
326
|
}
|
|
436
327
|
`;
|
|
437
328
|
}
|
|
@@ -441,11 +332,7 @@ export default class ${className} extends Model {
|
|
|
441
332
|
|
|
442
333
|
class ${className} extends Model {
|
|
443
334
|
static table = '${tableName}';
|
|
444
|
-
static
|
|
445
|
-
|
|
446
|
-
fillable = [];
|
|
447
|
-
|
|
448
|
-
// Define pivot relationships here
|
|
335
|
+
static fillable = [];
|
|
449
336
|
}
|
|
450
337
|
|
|
451
338
|
export default ${className};
|
|
@@ -456,11 +343,7 @@ export default ${className};
|
|
|
456
343
|
|
|
457
344
|
class ${className} extends Model {
|
|
458
345
|
static table = '${tableName}';
|
|
459
|
-
static
|
|
460
|
-
|
|
461
|
-
fillable = [];
|
|
462
|
-
|
|
463
|
-
// Define pivot relationships here
|
|
346
|
+
static fillable = [];
|
|
464
347
|
}
|
|
465
348
|
|
|
466
349
|
module.exports = ${className};
|
|
@@ -477,29 +360,27 @@ function generateFactory(className) {
|
|
|
477
360
|
}
|
|
478
361
|
|
|
479
362
|
const modelPath = structure.hasSrc ? `../models/${className}.js` : `../../models/${className}.js`;
|
|
363
|
+
const isESModule = isESModuleProject();
|
|
480
364
|
const template = isTypeScriptProject() ?
|
|
481
365
|
`import { defineFactory } from 'ilana-orm/orm/Factory.js';
|
|
482
366
|
import ${className} from '${modelPath}';
|
|
483
367
|
|
|
484
368
|
export default defineFactory(${className}, (faker) => ({
|
|
485
|
-
// Define your factory attributes here
|
|
486
369
|
// name: faker.person.fullName(),
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
370
|
+
}));
|
|
371
|
+
` : isESModule ?
|
|
372
|
+
`import { defineFactory } from 'ilana-orm/orm/Factory';
|
|
373
|
+
import ${className} from '${modelPath}';
|
|
374
|
+
|
|
375
|
+
export default defineFactory(${className}, (faker) => ({
|
|
376
|
+
// name: faker.person.fullName(),
|
|
491
377
|
}));
|
|
492
378
|
` :
|
|
493
379
|
`const { defineFactory } = require('ilana-orm/orm/Factory');
|
|
494
380
|
const ${className} = require('${modelPath.replace('.js', '')}');
|
|
495
381
|
|
|
496
382
|
module.exports = defineFactory(${className}, (faker) => ({
|
|
497
|
-
// Define your factory attributes here
|
|
498
383
|
// name: faker.person.fullName(),
|
|
499
|
-
// email: faker.internet.email(),
|
|
500
|
-
}))
|
|
501
|
-
.state('example', (faker) => ({
|
|
502
|
-
// Define state modifications here
|
|
503
384
|
}));
|
|
504
385
|
`;
|
|
505
386
|
|
|
@@ -517,34 +398,38 @@ function generateSeeder(className) {
|
|
|
517
398
|
}
|
|
518
399
|
|
|
519
400
|
const modelPath = structure.hasSrc ? `../models/${className}.js` : `../../models/${className}.js`;
|
|
401
|
+
const isESModule = isESModuleProject();
|
|
520
402
|
const template = isTypeScriptProject() ?
|
|
521
403
|
`import Seeder from 'ilana-orm/orm/Seeder.js';
|
|
404
|
+
import { factory } from 'ilana-orm/orm/Factory.js';
|
|
522
405
|
import ${className} from '${modelPath}';
|
|
523
406
|
import '../factories/${className}Factory.js';
|
|
524
407
|
|
|
525
408
|
export default class ${className}Seeder extends Seeder {
|
|
526
409
|
async run(): Promise<void> {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
410
|
+
await factory(${className}).times(10).create();
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
` : isESModule ?
|
|
414
|
+
`import Seeder from 'ilana-orm/orm/Seeder';
|
|
415
|
+
import { factory } from 'ilana-orm/orm/Factory';
|
|
416
|
+
import ${className} from '${modelPath}';
|
|
417
|
+
import '../factories/${className}Factory.js';
|
|
531
418
|
|
|
532
|
-
|
|
419
|
+
export default class ${className}Seeder extends Seeder {
|
|
420
|
+
async run() {
|
|
421
|
+
await factory(${className}).times(10).create();
|
|
533
422
|
}
|
|
534
423
|
}
|
|
535
424
|
` :
|
|
536
425
|
`const Seeder = require('ilana-orm/orm/Seeder');
|
|
426
|
+
const { factory } = require('ilana-orm/orm/Factory');
|
|
537
427
|
const ${className} = require('${modelPath.replace('.js', '')}');
|
|
538
428
|
require('../factories/${className}Factory');
|
|
539
429
|
|
|
540
430
|
class ${className}Seeder extends Seeder {
|
|
541
431
|
async run() {
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
// Create sample data
|
|
545
|
-
await ${className}.factory().times(10).create();
|
|
546
|
-
|
|
547
|
-
console.log('${className}s seeded successfully');
|
|
432
|
+
await factory(${className}).times(10).create();
|
|
548
433
|
}
|
|
549
434
|
}
|
|
550
435
|
|
|
@@ -1590,4 +1475,5 @@ if (require.main === module) {
|
|
|
1590
1475
|
main();
|
|
1591
1476
|
}
|
|
1592
1477
|
|
|
1593
|
-
module.exports = commands;
|
|
1478
|
+
module.exports = commands;
|
|
1479
|
+
module.exports._templates = { getModelTemplate, getPivotModelTemplate };
|
|
@@ -207,6 +207,10 @@ class SchemaBuilder {
|
|
|
207
207
|
return Promise.resolve();
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
+
enableVectorExtension() {
|
|
211
|
+
return this.knex.raw('CREATE EXTENSION IF NOT EXISTS vector');
|
|
212
|
+
}
|
|
213
|
+
|
|
210
214
|
createEnum(name, values) {
|
|
211
215
|
if (this.knex.client.config.client === 'pg') {
|
|
212
216
|
return this.knex.raw(`CREATE TYPE ${name} AS ENUM (${values.map(v => `'${v}'`).join(', ')})`);
|
package/index.edge.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Edge runtime entry point — no auto-loading of ilana.config.js.
|
|
2
|
+
// Use this when running in Cloudflare Workers, Deno, Bun, or Next.js edge routes.
|
|
3
|
+
// You must call Database.configure(config) explicitly before using any models.
|
|
4
|
+
//
|
|
5
|
+
// import { Model, Database } from 'ilana-orm/edge';
|
|
6
|
+
// Database.configure({ default: 'pg', connections: { pg: { ... } } });
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
global.__ILANA_EDGE__ = true;
|
|
10
|
+
module.exports = require('./index.js');
|
package/index.edge.mjs
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Edge runtime entry point — ESM wrapper.
|
|
2
|
+
// Sets __ILANA_EDGE__ before any model is loaded so the auto-loader is skipped.
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
|
|
6
|
+
globalThis.__ILANA_EDGE__ = true;
|
|
7
|
+
const exports = require('./index.js');
|
|
8
|
+
|
|
9
|
+
export const {
|
|
10
|
+
Model,
|
|
11
|
+
QueryBuilder,
|
|
12
|
+
Collection,
|
|
13
|
+
Database,
|
|
14
|
+
DB,
|
|
15
|
+
SchemaBuilder,
|
|
16
|
+
MigrationRunner,
|
|
17
|
+
Seeder,
|
|
18
|
+
Factory,
|
|
19
|
+
defineFactory,
|
|
20
|
+
ModelNotFoundException,
|
|
21
|
+
F,
|
|
22
|
+
HasOne,
|
|
23
|
+
HasMany,
|
|
24
|
+
BelongsTo,
|
|
25
|
+
BelongsToMany,
|
|
26
|
+
HasManyThrough,
|
|
27
|
+
MorphTo,
|
|
28
|
+
MorphOne,
|
|
29
|
+
MorphMany,
|
|
30
|
+
MoneyCast,
|
|
31
|
+
EncryptedCast,
|
|
32
|
+
JsonCast,
|
|
33
|
+
ArrayCast,
|
|
34
|
+
DateCast,
|
|
35
|
+
} = exports;
|
|
36
|
+
|
|
37
|
+
export default exports.Model;
|
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;
|
|
@@ -53,6 +53,9 @@ export default class Model<TAttributes extends ModelAttributes = ModelAttributes
|
|
|
53
53
|
static strictLoading: boolean;
|
|
54
54
|
static touches: string[];
|
|
55
55
|
static enums: { [column: string]: string[] };
|
|
56
|
+
static embeddingColumn: string;
|
|
57
|
+
static embeddingDimensions: number;
|
|
58
|
+
static embeddingProvider?: (text: string) => Promise<number[]>;
|
|
56
59
|
|
|
57
60
|
// Instance properties
|
|
58
61
|
attributes: ModelAttributes;
|
|
@@ -86,15 +89,25 @@ export default class Model<TAttributes extends ModelAttributes = ModelAttributes
|
|
|
86
89
|
static oldest(column?: string): QueryBuilder;
|
|
87
90
|
static withTrashed(): QueryBuilder;
|
|
88
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>;
|
|
89
95
|
static upsert(data: any[], uniqueBy: string[], update?: string[]): Promise<any>;
|
|
90
96
|
static withoutGlobalScopes(): QueryBuilder;
|
|
91
97
|
static make(attributes?: ModelAttributes): Model;
|
|
92
98
|
static create(attributes?: ModelAttributes): Promise<Model>;
|
|
93
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>;
|
|
94
105
|
static insert(data: ModelAttributes | ModelAttributes[]): Promise<any>;
|
|
95
106
|
static destroy(ids: any | any[]): Promise<number>;
|
|
96
107
|
static truncate(): Promise<void>;
|
|
97
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>>;
|
|
98
111
|
static firstOrCreate(attributes: ModelAttributes, values?: ModelAttributes): Promise<Model>;
|
|
99
112
|
static firstOrNew(attributes: ModelAttributes, values?: ModelAttributes): Promise<Model>;
|
|
100
113
|
static updateOrCreate(attributes: ModelAttributes, values?: ModelAttributes): Promise<Model>;
|
|
@@ -156,6 +169,8 @@ export default class Model<TAttributes extends ModelAttributes = ModelAttributes
|
|
|
156
169
|
forceDelete(): Promise<boolean>;
|
|
157
170
|
fresh(): Promise<this | null>;
|
|
158
171
|
is(other: Model): boolean;
|
|
172
|
+
isNot(other: Model | null | undefined): boolean;
|
|
173
|
+
replicate(except?: string[]): this;
|
|
159
174
|
toJSON(): any;
|
|
160
175
|
|
|
161
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 ---
|
|
@@ -38,6 +40,8 @@ class Model {
|
|
|
38
40
|
static strictLoading = false;
|
|
39
41
|
static touches = [];
|
|
40
42
|
static enums = {};
|
|
43
|
+
static embeddingColumn = 'embedding';
|
|
44
|
+
static embeddingDimensions = 1536;
|
|
41
45
|
|
|
42
46
|
// --- Instance props ---
|
|
43
47
|
attributes = {};
|
|
@@ -225,6 +229,9 @@ class Model {
|
|
|
225
229
|
static oldest(col) { return this.query().oldest(col || this.createdAt || 'created_at'); }
|
|
226
230
|
static withTrashed() { return this.query().withTrashed(); }
|
|
227
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); }
|
|
228
235
|
static async upsert(data, uniqueBy, update) { return this.query().upsert(data, uniqueBy, update); }
|
|
229
236
|
static withoutGlobalScopes() {
|
|
230
237
|
return new QueryBuilder(this.getTableName(), this, this.getConnectionName());
|
|
@@ -233,8 +240,8 @@ class Model {
|
|
|
233
240
|
static make(attrs = {}) {
|
|
234
241
|
const inst = new this(attrs);
|
|
235
242
|
inst._initialize();
|
|
236
|
-
if (!this.incrementing && this.keyType === 'string' && !inst.getKey()) {
|
|
237
|
-
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());
|
|
238
245
|
}
|
|
239
246
|
return inst;
|
|
240
247
|
}
|
|
@@ -252,8 +259,63 @@ class Model {
|
|
|
252
259
|
});
|
|
253
260
|
}
|
|
254
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
|
+
|
|
255
281
|
static async insert(data) { return this.query().insert(data); }
|
|
256
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
|
+
}
|
|
257
319
|
static async seed(count = 1) {
|
|
258
320
|
const { factory } = require('./Factory');
|
|
259
321
|
return factory(this).times(count).create();
|
|
@@ -341,6 +403,7 @@ class Model {
|
|
|
341
403
|
}
|
|
342
404
|
// static async fireEvent(evt, mdl) { for (const h of this.events[evt] || []) if (await h(mdl) === false) return false; }
|
|
343
405
|
static async fireEvent(evt, mdl) {
|
|
406
|
+
if (this._mutingEvents) return true;
|
|
344
407
|
const ownEvents = Object.hasOwn(this, 'events') ? this.events : {};
|
|
345
408
|
const handlers = ownEvents[evt] || [];
|
|
346
409
|
for (const handler of handlers) {
|
|
@@ -349,6 +412,31 @@ class Model {
|
|
|
349
412
|
return true;
|
|
350
413
|
}
|
|
351
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
|
+
|
|
352
440
|
// --- Instance methods ---
|
|
353
441
|
static getTableName() { return this.table || this.name.toLowerCase() + 's'; }
|
|
354
442
|
static getPrimaryKey() { return this.primaryKey; }
|
|
@@ -531,9 +619,10 @@ class Model {
|
|
|
531
619
|
this.setAttribute(createdAtCol, now).setAttribute(updatedAtCol, now);
|
|
532
620
|
}
|
|
533
621
|
|
|
534
|
-
// Generate UUID if needed
|
|
535
|
-
|
|
536
|
-
|
|
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());
|
|
537
626
|
}
|
|
538
627
|
|
|
539
628
|
const qb = this.constructor.query();
|
|
@@ -704,6 +793,27 @@ class Model {
|
|
|
704
793
|
return this.getKey() === other.getKey();
|
|
705
794
|
}
|
|
706
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
|
+
|
|
707
817
|
// JSON serialization
|
|
708
818
|
toJSON() {
|
|
709
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>>;
|
package/orm/QueryBuilder.js
CHANGED
|
@@ -227,7 +227,32 @@ class QueryBuilder {
|
|
|
227
227
|
}
|
|
228
228
|
|
|
229
229
|
addSelect(...columns) {
|
|
230
|
-
|
|
230
|
+
for (const col of columns) {
|
|
231
|
+
if (col && typeof col === 'object' && !Array.isArray(col)) {
|
|
232
|
+
// Subquery form: addSelect({ alias: (qb) => qb.from('table').select('col').where(...) })
|
|
233
|
+
for (const [alias, subFn] of Object.entries(col)) {
|
|
234
|
+
const knex = Database.connection(this.connectionName);
|
|
235
|
+
const subQuery = knex.queryBuilder();
|
|
236
|
+
const subQb = new QueryBuilder('', null, this.connectionName);
|
|
237
|
+
subQb.query = subQuery;
|
|
238
|
+
subFn(subQb);
|
|
239
|
+
this.query.column(knex.raw(`(${subQuery.toSQL().sql}) as "${alias}"`, subQuery.toSQL().bindings));
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
this.query.column(col);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return this;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
orderBySubquery(callback, direction = 'asc') {
|
|
249
|
+
const knex = Database.connection(this.connectionName);
|
|
250
|
+
const subQuery = knex.queryBuilder();
|
|
251
|
+
const subQb = new QueryBuilder('', null, this.connectionName);
|
|
252
|
+
subQb.query = subQuery;
|
|
253
|
+
callback(subQb);
|
|
254
|
+
const { sql, bindings } = subQuery.toSQL();
|
|
255
|
+
this.query.orderByRaw(`(${sql}) ${direction === 'desc' ? 'desc' : 'asc'}`, bindings);
|
|
231
256
|
return this;
|
|
232
257
|
}
|
|
233
258
|
|
|
@@ -250,6 +275,23 @@ class QueryBuilder {
|
|
|
250
275
|
return this;
|
|
251
276
|
}
|
|
252
277
|
|
|
278
|
+
withPendingAttributes(attrs) {
|
|
279
|
+
this._pendingAttributes = { ...(this._pendingAttributes || {}), ...attrs };
|
|
280
|
+
return this;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async new(attrs = {}) {
|
|
284
|
+
const merged = { ...(this._pendingAttributes || {}), ...attrs };
|
|
285
|
+
const inst = new this.modelClass(merged);
|
|
286
|
+
inst._initialize();
|
|
287
|
+
return inst;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async create(attrs = {}) {
|
|
291
|
+
const merged = { ...(this._pendingAttributes || {}), ...attrs };
|
|
292
|
+
return this.modelClass.create(merged);
|
|
293
|
+
}
|
|
294
|
+
|
|
253
295
|
when(condition, callback, otherwise) {
|
|
254
296
|
if (condition) {
|
|
255
297
|
callback(this, condition);
|
|
@@ -549,6 +591,37 @@ class QueryBuilder {
|
|
|
549
591
|
return this.whereDoesntHave(relation);
|
|
550
592
|
}
|
|
551
593
|
|
|
594
|
+
has(relation, operator = '>=', count = 1) {
|
|
595
|
+
if (!this.modelClass) return this;
|
|
596
|
+
try {
|
|
597
|
+
const dummy = this._makeDummy();
|
|
598
|
+
const relFn = this.modelClass.prototype[relation];
|
|
599
|
+
if (typeof relFn !== 'function') return this;
|
|
600
|
+
const rel = relFn.call(dummy);
|
|
601
|
+
const relatedClass = rel.getRelatedClass();
|
|
602
|
+
const relatedTable = relatedClass.getTableName();
|
|
603
|
+
const parentTable = this.modelClass.getTableName();
|
|
604
|
+
const isBelongsTo = rel.constructor.name === 'BelongsTo';
|
|
605
|
+
const joinCol = isBelongsTo
|
|
606
|
+
? `${relatedTable}.${rel.localKey || relatedClass.getPrimaryKey()} = ${parentTable}.${rel.foreignKey}`
|
|
607
|
+
: `${relatedTable}.${rel.foreignKey} = ${parentTable}.${rel.localKey || this.modelClass.getPrimaryKey()}`;
|
|
608
|
+
const ops = ['=', '!=', '<', '<=', '>', '>='];
|
|
609
|
+
const safeOp = ops.includes(operator) ? operator : '>=';
|
|
610
|
+
const safeCount = parseInt(count, 10) || 1;
|
|
611
|
+
if (safeOp === '>=' && safeCount === 1) {
|
|
612
|
+
this.query.whereExists((builder) => {
|
|
613
|
+
builder.from(relatedTable).whereRaw(joinCol);
|
|
614
|
+
});
|
|
615
|
+
} else {
|
|
616
|
+
this.query.whereRaw(
|
|
617
|
+
`(select count(*) from ?? where ${joinCol}) ${safeOp} ?`,
|
|
618
|
+
[relatedTable, safeCount]
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
} catch (_) {}
|
|
622
|
+
return this;
|
|
623
|
+
}
|
|
624
|
+
|
|
552
625
|
_makeDummy() {
|
|
553
626
|
const dummy = Object.create(this.modelClass.prototype);
|
|
554
627
|
dummy.attributes = {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ilana-orm",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.19",
|
|
4
4
|
"description": "A fully-featured, Eloquent-style ORM for Node.js with TypeScript support",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
"require": "./index.js",
|
|
11
11
|
"types": "./index.d.ts"
|
|
12
12
|
},
|
|
13
|
+
"./edge": {
|
|
14
|
+
"import": "./index.edge.mjs",
|
|
15
|
+
"require": "./index.edge.js",
|
|
16
|
+
"types": "./index.d.ts"
|
|
17
|
+
},
|
|
13
18
|
"./orm/Model": {
|
|
14
19
|
"import": "./orm/Model.mjs",
|
|
15
20
|
"require": "./orm/Model.js",
|