ilana-orm 1.0.0

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.
@@ -0,0 +1,299 @@
1
+ const QueryBuilder = require('./QueryBuilder');
2
+ const Model = require('./Model');
3
+ const ModelRegistry = require('./ModelRegistry');
4
+
5
+ class Relation {
6
+ constructor(parent, related, foreignKey, localKey = 'id') {
7
+ this.parent = parent;
8
+ this.related = related;
9
+ this.foreignKey = foreignKey;
10
+ this.localKey = localKey;
11
+ }
12
+
13
+ getRelatedClass() {
14
+ // If it's a string, resolve from registry
15
+ if (typeof this.related === 'string') {
16
+ return this.parent.constructor.resolveRelatedModel(this.related);
17
+ }
18
+
19
+ // If it's a function (class), check if it's properly initialized
20
+ if (typeof this.related === 'function') {
21
+ // For classes that might not be fully loaded, try to get from registry first
22
+ if (this.related.name && ModelRegistry.has(this.related.name)) {
23
+ return ModelRegistry.get(this.related.name);
24
+ }
25
+
26
+ // If not in registry but has required methods, use it directly
27
+ if (this.related.getTableName && typeof this.related.getTableName === 'function') {
28
+ return this.related;
29
+ }
30
+
31
+ // If it's a lazy loading function
32
+ if (this.related.length === 0) {
33
+ const resolved = this.related();
34
+ return resolved;
35
+ }
36
+ }
37
+
38
+ return this.related;
39
+ }
40
+
41
+ newQuery() {
42
+ const relatedClass = this.getRelatedClass();
43
+ return new QueryBuilder(relatedClass.getTableName(), relatedClass, relatedClass.getConnectionName());
44
+ }
45
+ }
46
+
47
+ class HasOne extends Relation {
48
+ addConstraints() {
49
+ // Constraints added when executing query
50
+ }
51
+
52
+ getRelatedClass() {
53
+ return super.getRelatedClass();
54
+ }
55
+
56
+ async getResults() {
57
+ return this.newQuery()
58
+ .where(this.foreignKey, this.parent.getAttribute(this.localKey))
59
+ .first();
60
+ }
61
+ }
62
+
63
+ class HasMany extends Relation {
64
+ addConstraints() {
65
+ // Constraints added when executing query
66
+ }
67
+
68
+ getRelatedClass() {
69
+ return super.getRelatedClass();
70
+ }
71
+
72
+ async getResults() {
73
+ return this.newQuery()
74
+ .where(this.foreignKey, this.parent.getAttribute(this.localKey))
75
+ .get();
76
+ }
77
+ }
78
+
79
+ class BelongsTo extends Relation {
80
+ addConstraints() {
81
+ // Constraints added when executing query
82
+ }
83
+
84
+ getRelatedClass() {
85
+ return super.getRelatedClass();
86
+ }
87
+
88
+ async getResults() {
89
+ return this.newQuery()
90
+ .where(this.localKey, this.parent.getAttribute(this.foreignKey))
91
+ .first();
92
+ }
93
+ }
94
+
95
+ class BelongsToMany extends Relation {
96
+ constructor(
97
+ parent,
98
+ related,
99
+ pivotTable,
100
+ foreignPivotKey,
101
+ relatedPivotKey,
102
+ parentKey = 'id',
103
+ relatedKey = 'id'
104
+ ) {
105
+ super(parent, related, relatedKey, parentKey);
106
+
107
+ const relatedClass = this.getRelatedClass();
108
+
109
+ this.pivotTable = pivotTable || [
110
+ parent.constructor.getTableName(),
111
+ relatedClass.getTableName()
112
+ ].sort().join('_');
113
+
114
+ this.parentPivotKey = foreignPivotKey || `${parent.constructor.getTableName().slice(0, -1)}_id`;
115
+ this.relatedPivotKey = relatedPivotKey || `${relatedClass.getTableName().slice(0, -1)}_id`;
116
+ this.parentKey = parentKey || parent.constructor.getPrimaryKey();
117
+ this.relatedKey = relatedKey || relatedClass.getPrimaryKey();
118
+ this.pivotColumns = [];
119
+ this.pivotTimestamps = false;
120
+ }
121
+
122
+ withPivot(...columns) {
123
+ this.pivotColumns.push(...columns);
124
+ return this;
125
+ }
126
+
127
+ withTimestamps() {
128
+ this.pivotTimestamps = true;
129
+ this.pivotColumns.push('created_at', 'updated_at');
130
+ return this;
131
+ }
132
+
133
+ addConstraints() {
134
+ // Constraints added when executing query
135
+ }
136
+
137
+ async getResults() {
138
+ const relatedClass = this.getRelatedClass();
139
+ const selectColumns = [`${relatedClass.getTableName()}.*`];
140
+
141
+ // Add pivot columns
142
+ if (this.pivotColumns.length > 0) {
143
+ for (const column of this.pivotColumns) {
144
+ selectColumns.push(`${this.pivotTable}.${column} as pivot_${column}`);
145
+ }
146
+ }
147
+
148
+ const query = this.newQuery()
149
+ .select(...selectColumns)
150
+ .join(this.pivotTable, `${relatedClass.getTableName()}.${this.relatedKey}`, `${this.pivotTable}.${this.relatedPivotKey}`)
151
+ .where(`${this.pivotTable}.${this.parentPivotKey}`, this.parent.getAttribute(this.parentKey));
152
+
153
+ const results = await query.get();
154
+
155
+ // Convert Collection to array if needed
156
+ const resultsArray = Array.isArray(results) ? results : Array.from(results);
157
+
158
+ // Add pivot data to models
159
+ return resultsArray.map((model) => {
160
+ const pivotData = {};
161
+ for (const column of this.pivotColumns) {
162
+ if (model.attributes[`pivot_${column}`] !== undefined) {
163
+ pivotData[column] = model.attributes[`pivot_${column}`];
164
+ delete model.attributes[`pivot_${column}`];
165
+ }
166
+ }
167
+ model.pivot = pivotData;
168
+ return model;
169
+ });
170
+ }
171
+
172
+ async attach(id, attributes = {}) {
173
+ const pivotData = {
174
+ [this.parentPivotKey]: this.parent.getAttribute(this.parentKey),
175
+ [this.relatedPivotKey]: id,
176
+ ...attributes
177
+ };
178
+
179
+ if (this.pivotTimestamps) {
180
+ const now = new Date();
181
+ pivotData.created_at = now;
182
+ pivotData.updated_at = now;
183
+ }
184
+
185
+ await new QueryBuilder(this.pivotTable).insert(pivotData);
186
+ }
187
+
188
+ async detach(id) {
189
+ const query = new QueryBuilder(this.pivotTable)
190
+ .where(this.parentPivotKey, this.parent.getAttribute(this.parentKey));
191
+
192
+ if (id !== undefined) {
193
+ query.where(this.relatedPivotKey, id);
194
+ }
195
+
196
+ return query.delete();
197
+ }
198
+
199
+ async sync(ids) {
200
+ await this.detach();
201
+ for (const id of ids) {
202
+ await this.attach(id);
203
+ }
204
+ }
205
+ }
206
+
207
+ class HasManyThrough extends Relation {
208
+ constructor(
209
+ parent,
210
+ related,
211
+ through,
212
+ firstKey,
213
+ secondKey,
214
+ localKey = 'id',
215
+ secondLocalKey = 'id'
216
+ ) {
217
+ super(parent, related, secondKey, localKey);
218
+ this.through = through;
219
+ this.firstKey = firstKey;
220
+ this.secondKey = secondKey;
221
+ this.localKey = localKey;
222
+ this.secondLocalKey = secondLocalKey;
223
+ }
224
+
225
+ addConstraints() {
226
+ // Constraints added when executing query
227
+ }
228
+
229
+ async getResults() {
230
+ return this.newQuery()
231
+ .select(`${this.related.getTableName()}.*`)
232
+ .join(this.through.getTableName(), `${this.related.getTableName()}.${this.secondLocalKey}`, `${this.through.getTableName()}.${this.secondKey}`)
233
+ .where(`${this.through.getTableName()}.${this.firstKey}`, this.parent.getAttribute(this.localKey))
234
+ .get();
235
+ }
236
+ }
237
+
238
+ // Polymorphic Relations
239
+ class MorphTo extends Relation {
240
+ constructor(parent, morphType, morphId) {
241
+ super(parent, Model, morphId, 'id');
242
+ this.morphType = morphType;
243
+ this.morphId = morphId;
244
+ }
245
+
246
+ addConstraints() {
247
+ // Constraints added when executing query
248
+ }
249
+
250
+ async getResults() {
251
+ const morphType = this.parent.getAttribute(this.morphType);
252
+ const morphId = this.parent.getAttribute(this.morphId);
253
+
254
+ if (!morphType || !morphId) {
255
+ return null;
256
+ }
257
+
258
+ // Resolve model class from registry
259
+ const ModelClass = ModelRegistry.get(morphType);
260
+ if (!ModelClass) {
261
+ throw new Error(`Model '${morphType}' not found in registry. Register it using ModelRegistry.register()`);
262
+ }
263
+
264
+ return new QueryBuilder(ModelClass.getTableName(), ModelClass)
265
+ .where(ModelClass.getPrimaryKey(), morphId)
266
+ .first();
267
+ }
268
+ }
269
+
270
+ class MorphMany extends Relation {
271
+ constructor(parent, related, morphType, morphId, morphClass) {
272
+ super(parent, related, morphId, 'id');
273
+ this.morphType = morphType;
274
+ this.morphId = morphId;
275
+ this.morphClass = morphClass;
276
+ }
277
+
278
+ addConstraints() {
279
+ // Constraints added when executing query
280
+ }
281
+
282
+ async getResults() {
283
+ return this.newQuery()
284
+ .where(this.morphType, this.morphClass)
285
+ .where(this.morphId, this.parent.getAttribute(this.localKey))
286
+ .get();
287
+ }
288
+ }
289
+
290
+ module.exports = {
291
+ Relation,
292
+ HasOne,
293
+ HasMany,
294
+ BelongsTo,
295
+ BelongsToMany,
296
+ HasManyThrough,
297
+ MorphTo,
298
+ MorphMany
299
+ };
package/orm/Seeder.js ADDED
@@ -0,0 +1,153 @@
1
+ const Database = require('../database/connection');
2
+
3
+ class Seeder {
4
+ constructor() {
5
+ this.db = Database;
6
+ this.batchSize = 1000;
7
+ }
8
+
9
+ // Batch processing utilities
10
+ async createInBatches(factory, count, attributes = {}) {
11
+ const results = [];
12
+ const batches = Math.ceil(count / this.batchSize);
13
+
14
+ for (let i = 0; i < batches; i++) {
15
+ const currentBatchSize = Math.min(this.batchSize, count - (i * this.batchSize));
16
+ const batch = await factory.times(currentBatchSize).create(attributes);
17
+ results.push(...(Array.isArray(batch) ? batch : [batch]));
18
+
19
+ if (batches > 1) {
20
+ console.log(`Batch ${i + 1}/${batches} completed (${results.length}/${count})`);
21
+ }
22
+ }
23
+
24
+ return results;
25
+ }
26
+
27
+ async disableForeignKeyChecks() {
28
+ const client = this.db.connection(this.connection).client.config.client;
29
+
30
+ if (client === 'mysql2') {
31
+ await this.db.raw('SET FOREIGN_KEY_CHECKS = 0');
32
+ } else if (client === 'pg') {
33
+ await this.db.raw('SET session_replication_role = replica');
34
+ }
35
+ }
36
+
37
+ async enableForeignKeyChecks() {
38
+ const client = this.db.connection(this.connection).client.config.client;
39
+
40
+ if (client === 'mysql2') {
41
+ await this.db.raw('SET FOREIGN_KEY_CHECKS = 1');
42
+ } else if (client === 'pg') {
43
+ await this.db.raw('SET session_replication_role = DEFAULT');
44
+ }
45
+ }
46
+
47
+ async call(seeders) {
48
+ // Handle both single seeder and array of seeders
49
+ const seederArray = Array.isArray(seeders) ? seeders : [seeders];
50
+
51
+ for (const SeederClass of seederArray) {
52
+ const seeder = new SeederClass();
53
+ if (this.connection) {
54
+ seeder.connection = this.connection;
55
+ }
56
+ await seeder.run();
57
+ console.log(`Seeded: ${SeederClass.name}`);
58
+ }
59
+ }
60
+
61
+ async callWith(seeders, connection) {
62
+ for (const [name, SeederClass] of Object.entries(seeders)) {
63
+ const seeder = new SeederClass();
64
+ if (connection || this.connection) {
65
+ seeder.connection = connection || this.connection;
66
+ }
67
+ await seeder.run();
68
+ console.log(`Seeded: ${name}`);
69
+ }
70
+ }
71
+
72
+ async callOnce(SeederClass, identifier) {
73
+ const tableName = 'seeder_log';
74
+
75
+ // Ensure seeder log table exists
76
+ const knex = this.db.getInstance();
77
+ const hasTable = await knex.schema.hasTable(tableName);
78
+ if (!hasTable) {
79
+ await knex.schema.createTable(tableName, (table) => {
80
+ table.increments('id');
81
+ table.string('seeder');
82
+ table.timestamp('executed_at').defaultTo(knex.fn.now());
83
+ });
84
+ }
85
+
86
+ // Check if already executed
87
+ const exists = await this.db.table(tableName).where('seeder', identifier).first();
88
+ if (exists) {
89
+ console.log(`Skipped: ${identifier} (already executed)`);
90
+ return;
91
+ }
92
+
93
+ // Execute seeder
94
+ const seeder = new SeederClass();
95
+ if (this.connection) {
96
+ seeder.connection = this.connection;
97
+ }
98
+ await seeder.run();
99
+
100
+ // Log execution
101
+ await this.db.table(tableName).insert({
102
+ seeder: identifier,
103
+ executed_at: new Date()
104
+ });
105
+
106
+ console.log(`Seeded: ${identifier}`);
107
+ }
108
+
109
+ async progress(total, callback) {
110
+ let completed = 0;
111
+ const updateProgress = (count) => {
112
+ completed += count;
113
+ const percentage = Math.round((completed / total) * 100);
114
+ console.log(`Progress: ${percentage}% (${completed}/${total})`);
115
+ };
116
+
117
+ await callback(updateProgress);
118
+ }
119
+
120
+ async truncate(table) {
121
+ await this.db.table(table, this.connection).truncate();
122
+ }
123
+
124
+ async truncateInOrder(tables) {
125
+ await this.disableForeignKeyChecks();
126
+
127
+ for (const table of tables) {
128
+ await this.truncate(table);
129
+ }
130
+
131
+ await this.enableForeignKeyChecks();
132
+ }
133
+
134
+ async wipeDatabase() {
135
+ const knex = this.db.connection(this.connection);
136
+ let tables = [];
137
+
138
+ if (knex.client.config.client === 'sqlite3') {
139
+ const result = await knex.raw("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'");
140
+ tables = result.map((row) => row.name);
141
+ } else if (knex.client.config.client === 'mysql2') {
142
+ const result = await knex.raw('SHOW TABLES');
143
+ tables = result[0].map((row) => Object.values(row)[0]);
144
+ } else if (knex.client.config.client === 'pg') {
145
+ const result = await knex.raw("SELECT tablename FROM pg_tables WHERE schemaname = 'public'");
146
+ tables = result.rows.map((row) => row.tablename);
147
+ }
148
+
149
+ await this.truncateInOrder(tables);
150
+ }
151
+ }
152
+
153
+ module.exports = Seeder;
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "ilana-orm",
3
+ "version": "1.0.0",
4
+ "description": "A fully-featured, Eloquent-style ORM for Node.js with TypeScript support",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "ilana": "cli/ilana.js"
8
+ },
9
+ "scripts": {
10
+ "build": "echo 'No build needed - using JavaScript files directly'",
11
+ "test": "jest",
12
+ "prepublishOnly": "echo 'Ready to publish'"
13
+ },
14
+ "keywords": [
15
+ "orm",
16
+ "nodejs",
17
+ "javascript",
18
+ "typescript-orm",
19
+ "typescript",
20
+ "database",
21
+ "eloquent",
22
+ "mysql",
23
+ "postgresql",
24
+ "sqlite",
25
+ "query-builder",
26
+ "migrations",
27
+ "relationships"
28
+ ],
29
+ "author": "Raphael Abayomi <raphyabak@gmail.com>",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/raphyabak/ilana-orm.git"
34
+ },
35
+ "homepage": "https://github.com/raphyabak/ilana-orm#readme",
36
+ "bugs": {
37
+ "url": "https://github.com/raphyabak/ilana-orm/issues"
38
+ },
39
+ "files": [
40
+ "*.js",
41
+ "cli/**/*.js",
42
+ "database/**/*.js",
43
+ "orm/**/*.js",
44
+ "README.md",
45
+ "LICENSE",
46
+ "ilana.png"
47
+ ],
48
+ "dependencies": {
49
+ "knex": "^3.0.0",
50
+ "uuid": "^9.0.0",
51
+ "@faker-js/faker": "^8.0.0",
52
+ "pg": "^8.0.0",
53
+ "mysql2": "^3.0.0",
54
+ "sqlite3": "^5.0.0",
55
+ "dotenv": "^16.3.1",
56
+ "moment-timezone": "^0.6.0"
57
+ },
58
+ "devDependencies": {
59
+ "@types/node": "^20.0.0",
60
+ "@types/uuid": "^9.0.0",
61
+ "typescript": "^5.0.0",
62
+ "ts-node": "^10.0.0",
63
+ "ts-node-dev": "^2.0.0",
64
+ "jest": "^29.0.0",
65
+ "@types/jest": "^29.0.0",
66
+ "ts-jest": "^29.0.0"
67
+ },
68
+ "engines": {
69
+ "node": ">=16.0.0"
70
+ }
71
+ }
package/test-role.js ADDED
@@ -0,0 +1,26 @@
1
+ const Model = require('./orm/Model');
2
+
3
+ class Role extends Model {
4
+ static table = 'roles';
5
+ static timestamps = true;
6
+ static softDeletes = false;
7
+
8
+ fillable = ['name', 'description', 'permissions'];
9
+ hidden = [];
10
+ casts = {
11
+ permissions: 'json',
12
+ created_at: 'date',
13
+ updated_at: 'date'
14
+ };
15
+
16
+ // Use string reference instead of importing User
17
+ users() {
18
+ return this.belongsToMany('User', 'user_roles', 'role_id', 'user_id');
19
+ }
20
+
21
+ static {
22
+ this.register();
23
+ }
24
+ }
25
+
26
+ module.exports = Role;