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 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 = 'string';
800
+ static keyType = 'uuid';
801
801
  static incrementing = false;
802
802
  }
803
803
 
804
- module.exports = User;
805
-
806
- // Usage
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
- **TypeScript:**
808
+ ### ULID Primary Keys
815
809
 
816
- ```typescript
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
- // Attributes
823
- id!: string; // UUID primary key
824
- name!: string;
825
- email!: string;
812
+ ```javascript
813
+ class Order extends Model {
814
+ static table = 'orders';
815
+ static keyType = 'ulid';
816
+ static incrementing = false;
826
817
  }
827
818
 
828
- // Usage
829
- const user = await User.create({
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.whereHas(relation, callback); // WHERE EXISTS subquery
4646
- query.doesntHave(relation); // WHERE NOT EXISTS subquery
4647
- query.whereDoesntHave(relation, callback); // WHERE NOT EXISTS with constraint
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 timestamps = true;
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 timestamps = true;
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 timestamps = true;
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 timestamps = true;
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 timestamps = true;
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 timestamps = true;
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
- // email: faker.internet.email(),
488
- }))
489
- .state('example', (faker) => ({
490
- // Define state modifications here
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
- console.log('Seeding ${className.toLowerCase()}s...');
528
-
529
- // Create sample data
530
- await ${className}.factory().times(10).create();
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
- console.log('${className}s seeded successfully');
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
- console.log('Seeding ${className.toLowerCase()}s...');
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;
@@ -417,28 +417,20 @@ class MigrationRunner {
417
417
 
418
418
  if (isCreate || name.includes('create_')) {
419
419
  return isTS ?
420
- `import type { SchemaBuilder } from 'ilana-orm';
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: SchemaBuilder): Promise<void> {
428
+ async down(schema) {
433
429
  await schema.dropTable('${table}');
434
430
  }
435
431
  }
436
432
  ` :
437
- `const { SchemaBuilder } = require('ilana-orm');
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
- `import type { SchemaBuilder } from 'ilana-orm';
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
- // Add your column modifications here
466
- // table.string('new_column').nullable();
453
+ // table.string('column_name').nullable();
467
454
  });
468
455
  }
469
456
 
470
- async down(schema: SchemaBuilder): Promise<void> {
457
+ async down(schema) {
471
458
  await schema.table('${table}', (table) => {
472
- // Reverse your modifications here
473
- // table.dropColumn('new_column');
459
+ // table.dropColumn('column_name');
474
460
  });
475
461
  }
476
462
  }
477
463
  ` :
478
- `const { SchemaBuilder } = require('ilana-orm');
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
- // Add your column modifications here
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
- // Reverse your modifications here
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
- `import type { SchemaBuilder } from 'ilana-orm';
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: SchemaBuilder): Promise<void> {
513
- // Add your rollback logic here
487
+ async down(schema) {
514
488
  }
515
489
  }
516
490
  ` :
517
- `const { SchemaBuilder } = require('ilana-orm');
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
- (function autoLoadConfig() {
9
- const fs = require('fs');
10
- const path = require('path');
11
- const configPathJs = path.join(process.cwd(), 'ilana.config.js');
12
- const configPathMjs = path.join(process.cwd(), 'ilana.config.mjs');
13
-
14
- if (fs.existsSync(configPathJs)) {
15
- delete require.cache[configPathJs];
16
- require(configPathJs);
17
- } else if (fs.existsSync(configPathMjs)) {
18
- // For ES modules, we'll handle this in the _getConfig method
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.generateUuid());
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
- if (!this.constructor.incrementing && this.constructor.keyType === 'string' && !this.getKey()) {
536
- this.setAttribute(this.constructor.primaryKey, this.constructor.generateUuid());
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();
@@ -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>>;
@@ -227,7 +227,32 @@ class QueryBuilder {
227
227
  }
228
228
 
229
229
  addSelect(...columns) {
230
- this.query.column(...columns);
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.18",
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",