ilana-orm 1.0.18 → 1.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -32
- package/cli/ilana.js +31 -145
- package/database/schema-builder.js +4 -0
- package/index.d.ts +8 -0
- package/index.edge.js +10 -0
- package/index.edge.mjs +38 -0
- package/index.js +2 -1
- package/index.mjs +1 -0
- package/orm/Errors.js +20 -1
- package/orm/Factory.js +2 -2
- package/orm/MigrationRunner.js +16 -48
- package/orm/Model.d.ts +128 -2
- package/orm/Model.js +187 -23
- package/orm/QueryBuilder.d.ts +10 -0
- package/orm/QueryBuilder.js +74 -1
- package/package.json +24 -7
package/README.md
CHANGED
|
@@ -5,7 +5,19 @@
|
|
|
5
5
|
|
|
6
6
|
**Ìlànà** (pronounced "ee-LAH-nah") - A Yoruba word meaning "pattern," "system," or "protocol."
|
|
7
7
|
|
|
8
|
-
A fully-featured, Eloquent-style ORM for Node.js
|
|
8
|
+
A fully-featured, Laravel Eloquent-style ORM for Node.js & TypeScript. If you know Eloquent, you already know IlanaORM — same API, same patterns, same conventions. MySQL, PostgreSQL, SQLite, Supabase, edge runtimes, and pgvector AI search out of the box.
|
|
9
|
+
|
|
10
|
+
| Feature | IlanaORM | Prisma | Drizzle | TypeORM |
|
|
11
|
+
|---|:---:|:---:|:---:|:---:|
|
|
12
|
+
| Eloquent-identical API | ✅ | ❌ | ❌ | ❌ |
|
|
13
|
+
| pgvector / AI search built-in | ✅ | ❌ | ❌ | ❌ |
|
|
14
|
+
| Edge runtime (Cloudflare, Next.js) | ✅ | ⚠️ | ✅ | ❌ |
|
|
15
|
+
| Supabase compatible | ✅ | ✅ | ✅ | ⚠️ |
|
|
16
|
+
| ULID primary keys | ✅ | ⚠️ | ⚠️ | ⚠️ |
|
|
17
|
+
| Factories & seeders built-in | ✅ | ❌ | ❌ | ❌ |
|
|
18
|
+
| Model events | ✅ | ⚠️ | ❌ | ✅ |
|
|
19
|
+
| Soft deletes | ✅ | ❌ | ❌ | ✅ |
|
|
20
|
+
| No code generation step | ✅ | ❌ | ✅ | ✅ |
|
|
9
21
|
|
|
10
22
|
## Table of Contents
|
|
11
23
|
|
|
@@ -24,6 +36,7 @@ A fully-featured, Eloquent-style ORM for Node.js with automatic TypeScript suppo
|
|
|
24
36
|
- [Schema Builder](#schema-builder)
|
|
25
37
|
- [Transactions](#transactions)
|
|
26
38
|
- [Advanced Features](#advanced-features)
|
|
39
|
+
- [Supabase](#supabase)
|
|
27
40
|
- [Complete API Reference](#complete-api-reference)
|
|
28
41
|
- [TypeScript Support](#typescript-support)
|
|
29
42
|
- [Performance & Best Practices](#performance--best-practices)
|
|
@@ -717,6 +730,11 @@ class User extends Model {
|
|
|
717
730
|
fillable = ["name", "email", "password"];
|
|
718
731
|
guarded = ["id", "created_at", "updated_at"];
|
|
719
732
|
|
|
733
|
+
// By default, fill()/update() silently drops any key not covered by
|
|
734
|
+
// fillable/guarded — e.g. update({ typo_column: x }) resolves successfully
|
|
735
|
+
// and writes nothing. Opt in to catch that instead of silently swallowing it:
|
|
736
|
+
static preventsSilentlyDiscardingAttributes = true; // throws MassAssignmentException on any discarded key
|
|
737
|
+
|
|
720
738
|
// Hidden attributes (won't appear in JSON)
|
|
721
739
|
hidden = ["password", "remember_token"];
|
|
722
740
|
|
|
@@ -793,47 +811,33 @@ export default class User extends Model {
|
|
|
793
811
|
|
|
794
812
|
### UUID Primary Keys
|
|
795
813
|
|
|
796
|
-
**JavaScript:**
|
|
797
814
|
```javascript
|
|
798
815
|
class User extends Model {
|
|
799
816
|
static table = 'users';
|
|
800
|
-
static keyType = '
|
|
817
|
+
static keyType = 'uuid';
|
|
801
818
|
static incrementing = false;
|
|
802
819
|
}
|
|
803
820
|
|
|
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
|
-
````
|
|
821
|
+
const user = await User.create({ name: 'John Doe', email: 'john@example.com' });
|
|
822
|
+
// user.id → "550e8400-e29b-41d4-a716-446655440000"
|
|
823
|
+
```
|
|
813
824
|
|
|
814
|
-
|
|
825
|
+
### ULID Primary Keys
|
|
815
826
|
|
|
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;
|
|
827
|
+
ULIDs are 26-character sortable identifiers — URL-safe, lexicographically ordered by creation time:
|
|
821
828
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
829
|
+
```javascript
|
|
830
|
+
class Order extends Model {
|
|
831
|
+
static table = 'orders';
|
|
832
|
+
static keyType = 'ulid';
|
|
833
|
+
static incrementing = false;
|
|
826
834
|
}
|
|
827
835
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
name: "John Doe",
|
|
831
|
-
email: "john@example.com",
|
|
832
|
-
});
|
|
833
|
-
// user.id will be a generated UUID
|
|
836
|
+
const order = await Order.create({ total: 49.99 });
|
|
837
|
+
// order.id → "01J3X7KQZB8YTPNMCHW4RSVFGE"
|
|
834
838
|
```
|
|
835
839
|
|
|
836
|
-
|
|
840
|
+
Use `char(26)` for the column type in migrations.
|
|
837
841
|
|
|
838
842
|
### Attribute Casting
|
|
839
843
|
|
|
@@ -4179,6 +4183,68 @@ const users = await User.query().select('id', 'name', 'email').values();
|
|
|
4179
4183
|
// [{ id: 1, name: 'John', email: 'john@example.com' }, ...]
|
|
4180
4184
|
```
|
|
4181
4185
|
|
|
4186
|
+
### Vector / Semantic Search (pgvector)
|
|
4187
|
+
|
|
4188
|
+
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.
|
|
4189
|
+
|
|
4190
|
+
**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.
|
|
4191
|
+
|
|
4192
|
+
```javascript
|
|
4193
|
+
// 1. Enable the extension in a migration
|
|
4194
|
+
await schema.enableVectorExtension();
|
|
4195
|
+
|
|
4196
|
+
// 2. Add a vector column
|
|
4197
|
+
await schema.table('posts', table => {
|
|
4198
|
+
table.specificType('embedding', 'vector(1536)'); // dimensions must match your model
|
|
4199
|
+
});
|
|
4200
|
+
|
|
4201
|
+
// 3. Configure the model
|
|
4202
|
+
class Post extends Model {
|
|
4203
|
+
static embeddingColumn = 'embedding';
|
|
4204
|
+
static embeddingProvider = async (text) => {
|
|
4205
|
+
// Must return Promise<number[]> — plug in any embedding API
|
|
4206
|
+
const res = await openai.embeddings.create({ model: 'text-embedding-ada-002', input: text });
|
|
4207
|
+
return res.data[0].embedding;
|
|
4208
|
+
};
|
|
4209
|
+
}
|
|
4210
|
+
|
|
4211
|
+
// 4. Store embeddings when creating records
|
|
4212
|
+
await Post.create({
|
|
4213
|
+
title: 'JavaScript tips',
|
|
4214
|
+
body: '...',
|
|
4215
|
+
embedding: JSON.stringify(await Post.embeddingProvider('JavaScript tips ...')),
|
|
4216
|
+
});
|
|
4217
|
+
|
|
4218
|
+
// 5. Search — converts text to vector, queries by similarity
|
|
4219
|
+
const posts = await Post.search('javascript performance tips', { limit: 5 });
|
|
4220
|
+
|
|
4221
|
+
// Or search by a raw vector you already have
|
|
4222
|
+
const posts = await Post.nearestTo(myVector, { distance: 'cosine', limit: 10 });
|
|
4223
|
+
|
|
4224
|
+
// Each result has a .distance attribute (lower = more similar)
|
|
4225
|
+
posts.forEach(p => console.log(p.title, p.distance));
|
|
4226
|
+
```
|
|
4227
|
+
|
|
4228
|
+
Distance options: `'cosine'` (default, `<=>`), `'l2'` (`<->`), `'inner'` (`<#>`).
|
|
4229
|
+
|
|
4230
|
+
### Edge Runtime
|
|
4231
|
+
|
|
4232
|
+
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:
|
|
4233
|
+
|
|
4234
|
+
```javascript
|
|
4235
|
+
import { Model, Database } from 'ilana-orm/edge';
|
|
4236
|
+
|
|
4237
|
+
// Must configure explicitly — no auto-loading of ilana.config.js
|
|
4238
|
+
Database.configure({
|
|
4239
|
+
default: 'pg',
|
|
4240
|
+
connections: {
|
|
4241
|
+
pg: { client: 'pg', connection: { connectionString: process.env.DATABASE_URL } },
|
|
4242
|
+
},
|
|
4243
|
+
});
|
|
4244
|
+
|
|
4245
|
+
const users = await User.all();
|
|
4246
|
+
```
|
|
4247
|
+
|
|
4182
4248
|
## TypeScript Support
|
|
4183
4249
|
|
|
4184
4250
|
### Type-Safe Models
|
|
@@ -4231,6 +4297,32 @@ const user = await User.factory().create({
|
|
|
4231
4297
|
});
|
|
4232
4298
|
```
|
|
4233
4299
|
|
|
4300
|
+
## Supabase
|
|
4301
|
+
|
|
4302
|
+
Supabase is a hosted PostgreSQL platform. IlanaORM works with it out of the box using the `pg` driver — no special setup required.
|
|
4303
|
+
|
|
4304
|
+
```javascript
|
|
4305
|
+
// ilana.config.js
|
|
4306
|
+
const { Database } = require('ilana-orm');
|
|
4307
|
+
|
|
4308
|
+
Database.configure({
|
|
4309
|
+
default: 'pg',
|
|
4310
|
+
connections: {
|
|
4311
|
+
pg: {
|
|
4312
|
+
client: 'pg',
|
|
4313
|
+
connection: {
|
|
4314
|
+
connectionString: process.env.DATABASE_URL, // from Supabase dashboard
|
|
4315
|
+
ssl: { rejectUnauthorized: false }, // required for Supabase
|
|
4316
|
+
},
|
|
4317
|
+
},
|
|
4318
|
+
},
|
|
4319
|
+
});
|
|
4320
|
+
```
|
|
4321
|
+
|
|
4322
|
+
All features work including migrations, relations, soft deletes, and vector search (Supabase has pgvector built in).
|
|
4323
|
+
|
|
4324
|
+
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.
|
|
4325
|
+
|
|
4234
4326
|
## Performance & Best Practices
|
|
4235
4327
|
|
|
4236
4328
|
### Query Optimization
|
|
@@ -4410,10 +4502,14 @@ User.upsert(data, unique, update); // Upsert records
|
|
|
4410
4502
|
User.destroy(ids); // Delete by IDs (soft-delete aware)
|
|
4411
4503
|
User.truncate(); // Delete all rows in the table
|
|
4412
4504
|
User.seed(n); // Create n records using the registered factory
|
|
4505
|
+
User.prune(); // Delete all records matching static prunable()
|
|
4413
4506
|
User.withTrashed(); // Include soft deleted
|
|
4414
4507
|
User.onlyTrashed(); // Only soft deleted
|
|
4415
4508
|
User.withoutTrashed(); // Exclude soft deleted
|
|
4416
4509
|
|
|
4510
|
+
// Events
|
|
4511
|
+
User.withoutEvents(async () => { ... }); // Run without firing events
|
|
4512
|
+
|
|
4417
4513
|
// Configuration
|
|
4418
4514
|
User.getTableName(); // Get table name
|
|
4419
4515
|
User.getPrimaryKey(); // Get primary key
|
|
@@ -4421,6 +4517,7 @@ User.getKeyType(); // Get key type
|
|
|
4421
4517
|
User.getIncrementing(); // Get incrementing flag
|
|
4422
4518
|
User.getConnectionName(); // Get connection name
|
|
4423
4519
|
User.generateUuid(); // Generate UUID
|
|
4520
|
+
User.generateUlid(); // Generate ULID
|
|
4424
4521
|
|
|
4425
4522
|
// Events
|
|
4426
4523
|
User.creating(callback); // Before creating
|
|
@@ -4458,9 +4555,12 @@ user.forceDelete(); // Force delete (ignores softDeletes)
|
|
|
4458
4555
|
user.restore(); // Restore soft deleted
|
|
4459
4556
|
user.fresh(); // Re-fetch from DB and return new instance
|
|
4460
4557
|
user.is(other); // Check if two instances are the same record
|
|
4558
|
+
user.isNot(other); // Inverse of is()
|
|
4559
|
+
user.replicate(except?); // Clone as unsaved record (excludes PK + timestamps)
|
|
4461
4560
|
|
|
4462
4561
|
// Attributes
|
|
4463
4562
|
user.fill(attributes); // Mass assign (respects fillable/guarded)
|
|
4563
|
+
user.forceFill(attributes); // Mass assign, bypassing fillable/guarded entirely
|
|
4464
4564
|
user.getAttribute(key); // Get attribute (calls accessor if defined)
|
|
4465
4565
|
user.setAttribute(key, value); // Set attribute (calls mutator if defined)
|
|
4466
4566
|
user.getKey(); // Get primary key value
|
|
@@ -4642,9 +4742,10 @@ query.upsert(data, uniqueBy, update);
|
|
|
4642
4742
|
query.with(...relations);
|
|
4643
4743
|
query.withConstraints(relation, callback);
|
|
4644
4744
|
query.withCount(...relations); // adds relation_count subquery column per model
|
|
4645
|
-
query.
|
|
4646
|
-
query.
|
|
4647
|
-
query.
|
|
4745
|
+
query.has(relation, operator?, count?); // WHERE EXISTS, or count-based (e.g. has('posts', '>', 5))
|
|
4746
|
+
query.whereHas(relation, callback); // WHERE EXISTS with constraint subquery
|
|
4747
|
+
query.doesntHave(relation); // WHERE NOT EXISTS subquery
|
|
4748
|
+
query.whereDoesntHave(relation, callback); // WHERE NOT EXISTS with constraint
|
|
4648
4749
|
```
|
|
4649
4750
|
|
|
4650
4751
|
#### 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.d.ts
CHANGED
|
@@ -22,5 +22,13 @@ export declare class ModelNotFoundException extends Error {
|
|
|
22
22
|
toResponse(): { status: 404; message: string };
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
export declare class MassAssignmentException extends Error {
|
|
26
|
+
name: 'MassAssignmentException';
|
|
27
|
+
model: string;
|
|
28
|
+
discardedKeys: string[];
|
|
29
|
+
constructor(model: string, discardedKeys: string[]);
|
|
30
|
+
toResponse(): { status: 422; message: string };
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
// Default export
|
|
26
34
|
export { default } from './orm/Model';
|
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,38 @@
|
|
|
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
|
+
MassAssignmentException,
|
|
22
|
+
F,
|
|
23
|
+
HasOne,
|
|
24
|
+
HasMany,
|
|
25
|
+
BelongsTo,
|
|
26
|
+
BelongsToMany,
|
|
27
|
+
HasManyThrough,
|
|
28
|
+
MorphTo,
|
|
29
|
+
MorphOne,
|
|
30
|
+
MorphMany,
|
|
31
|
+
MoneyCast,
|
|
32
|
+
EncryptedCast,
|
|
33
|
+
JsonCast,
|
|
34
|
+
ArrayCast,
|
|
35
|
+
DateCast,
|
|
36
|
+
} = exports;
|
|
37
|
+
|
|
38
|
+
export default exports.Model;
|
package/index.js
CHANGED
|
@@ -10,7 +10,7 @@ const Seeder = require('./orm/Seeder');
|
|
|
10
10
|
const Factory = require('./orm/Factory');
|
|
11
11
|
const Relation = require('./orm/Relation');
|
|
12
12
|
const CustomCasts = require('./orm/CustomCasts');
|
|
13
|
-
const { ModelNotFoundException } = require('./orm/Errors');
|
|
13
|
+
const { ModelNotFoundException, MassAssignmentException } = require('./orm/Errors');
|
|
14
14
|
const { F } = require('./orm/F');
|
|
15
15
|
|
|
16
16
|
module.exports = {
|
|
@@ -25,6 +25,7 @@ module.exports = {
|
|
|
25
25
|
Factory: Factory.Factory,
|
|
26
26
|
defineFactory: Factory.defineFactory,
|
|
27
27
|
ModelNotFoundException,
|
|
28
|
+
MassAssignmentException,
|
|
28
29
|
F,
|
|
29
30
|
|
|
30
31
|
// Relationships
|
package/index.mjs
CHANGED
package/orm/Errors.js
CHANGED
|
@@ -15,4 +15,23 @@ class ModelNotFoundException extends Error {
|
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
class MassAssignmentException extends Error {
|
|
19
|
+
constructor(model, discardedKeys) {
|
|
20
|
+
const keys = discardedKeys.join(', ');
|
|
21
|
+
super(
|
|
22
|
+
`Mass assignment blocked on ${model}: [${keys}] are not fillable, so ${model}.preventsSilentlyDiscardingAttributes ` +
|
|
23
|
+
`stopped the call instead of dropping them silently. Add the intended column(s) to ${model}.fillable, adjust ` +
|
|
24
|
+
`${model}.guarded, or set the attribute(s) directly (e.g. instance.column = value) instead of going through fill()/update().`
|
|
25
|
+
);
|
|
26
|
+
this.name = 'MassAssignmentException';
|
|
27
|
+
this.model = model;
|
|
28
|
+
this.discardedKeys = discardedKeys;
|
|
29
|
+
if (Error.captureStackTrace) Error.captureStackTrace(this, MassAssignmentException);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
toResponse() {
|
|
33
|
+
return { status: 422, message: this.message };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { ModelNotFoundException, MassAssignmentException };
|
package/orm/Factory.js
CHANGED
|
@@ -172,7 +172,7 @@ class Factory {
|
|
|
172
172
|
const modelAttributes = this.makeRaw(attributes);
|
|
173
173
|
|
|
174
174
|
const model = new this.model();
|
|
175
|
-
model.
|
|
175
|
+
model.forceFill(modelAttributes);
|
|
176
176
|
|
|
177
177
|
// Run afterMaking callbacks
|
|
178
178
|
for (const callback of this._afterMakingCallbacks) {
|
|
@@ -364,7 +364,7 @@ class BulkFactory {
|
|
|
364
364
|
|
|
365
365
|
for (let k = 0; k < batchData.length; k++) {
|
|
366
366
|
const model = new this.model();
|
|
367
|
-
model.
|
|
367
|
+
model.forceFill({ ...batchData[k], id: insertedIds[k] });
|
|
368
368
|
model.exists = true;
|
|
369
369
|
results.push(model);
|
|
370
370
|
}
|