ilana-orm 1.0.15 → 1.0.17

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/cli/ilana.js CHANGED
@@ -749,6 +749,271 @@ module.exports = ${className}Cast;
749
749
  }
750
750
 
751
751
 
752
+ // ─── Type Generator ───────────────────────────────────────────────────────────
753
+
754
+ const CAST_TYPE_MAP = {
755
+ string: 'string', number: 'number', float: 'number',
756
+ boolean: 'boolean', date: 'Date',
757
+ json: 'Record<string, any>', object: 'Record<string, any>', array: 'any[]',
758
+ };
759
+
760
+ // Knex column builder method → { tsType, nullable default }
761
+ const KNEX_COLUMN_MAP = {
762
+ increments: { type: 'number', nullable: false },
763
+ bigIncrements: { type: 'number', nullable: false },
764
+ integer: { type: 'number', nullable: true },
765
+ bigInteger: { type: 'number', nullable: true },
766
+ tinyint: { type: 'number', nullable: true },
767
+ smallint: { type: 'number', nullable: true },
768
+ mediumint: { type: 'number', nullable: true },
769
+ float: { type: 'number', nullable: true },
770
+ double: { type: 'number', nullable: true },
771
+ decimal: { type: 'number', nullable: true },
772
+ string: { type: 'string', nullable: true },
773
+ text: { type: 'string', nullable: true },
774
+ mediumtext: { type: 'string', nullable: true },
775
+ longtext: { type: 'string', nullable: true },
776
+ char: { type: 'string', nullable: true },
777
+ uuid: { type: 'string', nullable: true },
778
+ enum: { type: 'string', nullable: true },
779
+ set: { type: 'string', nullable: true },
780
+ boolean: { type: 'boolean', nullable: true },
781
+ date: { type: 'Date', nullable: true },
782
+ datetime: { type: 'Date', nullable: true },
783
+ timestamp: { type: 'Date', nullable: true },
784
+ time: { type: 'string', nullable: true },
785
+ json: { type: 'Record<string, any>', nullable: true },
786
+ jsonb: { type: 'Record<string, any>', nullable: true },
787
+ binary: { type: 'Buffer', nullable: true },
788
+ };
789
+
790
+ const RELATION_RETURN_MAP = {
791
+ hasOne: (r) => `${r} | null`,
792
+ hasMany: (r) => `${r}[]`,
793
+ belongsTo: (r) => `${r} | null`,
794
+ belongsToMany: (r) => `${r}[]`,
795
+ hasManyThrough: (r) => `${r}[]`,
796
+ morphTo: () => 'any',
797
+ morphOne: () => 'any | null',
798
+ morphMany: () => 'any[]',
799
+ };
800
+
801
+ // Parse migration files to extract column definitions for a given table name
802
+ function parseMigrationsForTable(tableName, migrationsDir) {
803
+ if (!fs.existsSync(migrationsDir)) return {};
804
+
805
+ const columns = {};
806
+ const files = fs.readdirSync(migrationsDir)
807
+ .filter(f => f.endsWith('.js') || f.endsWith('.mjs') || f.endsWith('.ts'))
808
+ .sort(); // oldest first so later migrations can override
809
+
810
+ for (const file of files) {
811
+ const content = fs.readFileSync(path.join(migrationsDir, file), 'utf8');
812
+
813
+ // Find createTable / table blocks for this table
814
+ const tableBlockRegex = new RegExp(
815
+ `(?:createTable|table)\\s*\\(\\s*['"]${tableName}['"]\\s*,\\s*(?:async\\s*)?(?:\\(\\s*)?(\\w+)\\s*(?:\\))?\\s*(?:=>|{)[\\s\\S]*?(?=\\}\\s*\\)|\\}\\s*;)`,
816
+ 'g'
817
+ );
818
+
819
+ let blockMatch;
820
+ while ((blockMatch = tableBlockRegex.exec(content)) !== null) {
821
+ const block = blockMatch[0];
822
+ const alias = blockMatch[1] || 'table';
823
+
824
+ // Match column definitions: table.string('col'), table.integer('col').nullable(), etc.
825
+ const colRegex = new RegExp(
826
+ `${alias}\\.(\\w+)\\s*\\(\\s*['"]([^'"]+)['"](?:[^)]*)?\\)([^;\\n]*)`,
827
+ 'g'
828
+ );
829
+
830
+ let colMatch;
831
+ while ((colMatch = colRegex.exec(block)) !== null) {
832
+ const [, method, colName, rest] = colMatch;
833
+ const knexDef = KNEX_COLUMN_MAP[method];
834
+ if (!knexDef) continue;
835
+
836
+ const isNullable = /\.nullable\(\)/.test(rest);
837
+ const isNotNullable = /\.notNullable\(\)/.test(rest);
838
+ const nullable = isNullable ? true : isNotNullable ? false : knexDef.nullable;
839
+
840
+ columns[colName] = { type: knexDef.type, nullable };
841
+ }
842
+ }
843
+ }
844
+
845
+ return columns;
846
+ }
847
+
848
+ function parseModelFile(content, fileName) {
849
+ const classMatch = content.match(/class\s+(\w+)\s+extends/);
850
+ const className = classMatch?.[1] || toPascalCase(path.basename(fileName, path.extname(fileName)));
851
+
852
+ const tableMatch = content.match(/static\s+table\s*=\s*['"]([^'"]+)['"]/);
853
+ const table = tableMatch?.[1] || pluralize(toSnakeCase(className));
854
+
855
+ const fillableMatch = content.match(/(?:static\s+)?fillable\s*=\s*\[([^\]]*)\]/s);
856
+ const fillable = fillableMatch
857
+ ? [...fillableMatch[1].matchAll(/['"]([^'"]+)['"]/g)].map(m => m[1])
858
+ : [];
859
+
860
+ const castsMatch = content.match(/(?:static\s+)?casts\s*=\s*\{([^}]*)\}/s);
861
+ const casts = {};
862
+ if (castsMatch) {
863
+ for (const [, key, val] of castsMatch[1].matchAll(/['"]?(\w+)['"]?\s*:\s*['"](\w+)['"]/g)) {
864
+ casts[key] = val;
865
+ }
866
+ }
867
+
868
+ const pkMatch = content.match(/static\s+primaryKey\s*=\s*['"]([^'"]+)['"]/);
869
+ const primaryKey = pkMatch?.[1] || 'id';
870
+
871
+ const keyTypeMatch = content.match(/static\s+keyType\s*=\s*['"]([^'"]+)['"]/);
872
+ const keyType = (keyTypeMatch?.[1] === 'string') ? 'string' : 'number';
873
+
874
+ const timestampsMatch = content.match(/static\s+timestamps\s*=\s*(true|false)/);
875
+ const timestamps = timestampsMatch?.[1] !== 'false';
876
+
877
+ const softDeletesMatch = content.match(/static\s+softDeletes\s*=\s*(true|false)/);
878
+ const softDeletes = softDeletesMatch?.[1] === 'true';
879
+
880
+ const createdAtCol = content.match(/static\s+createdAt\s*=\s*['"]([^'"]+)['"]/)?.[1] || 'created_at';
881
+ const updatedAtCol = content.match(/static\s+updatedAt\s*=\s*['"]([^'"]+)['"]/)?.[1] || 'updated_at';
882
+ const deletedAtCol = content.match(/static\s+deletedAt\s*=\s*['"]([^'"]+)['"]/)?.[1] || 'deleted_at';
883
+
884
+ const relations = [];
885
+ const relRegex = /(\w+)\s*\(\s*\)\s*\{[\s\S]*?return\s+this\.(hasOne|hasMany|belongsTo|belongsToMany|hasManyThrough|morphTo|morphOne|morphMany)\s*\(\s*['"]?(\w*?)['"]?[,)]/g;
886
+ let m;
887
+ while ((m = relRegex.exec(content)) !== null) {
888
+ const [, methodName, relationType, relatedModel] = m;
889
+ if (methodName !== 'constructor') {
890
+ relations.push({ methodName, relationType, relatedModel: relatedModel || 'any' });
891
+ }
892
+ }
893
+
894
+ return { className, table, fillable, casts, primaryKey, keyType, timestamps, softDeletes, createdAtCol, updatedAtCol, deletedAtCol, relations };
895
+ }
896
+
897
+ function generateModelTypes(model, migrationColumns = {}) {
898
+ const { className, fillable, casts, primaryKey, keyType, timestamps, softDeletes, createdAtCol, updatedAtCol, deletedAtCol, relations } = model;
899
+
900
+ const relatedModels = [...new Set(
901
+ relations.map(r => r.relatedModel).filter(r => r && r !== 'any')
902
+ )];
903
+
904
+ const fields = [];
905
+
906
+ // Primary key — always non-nullable
907
+ fields.push(` ${primaryKey}: ${keyType};`);
908
+
909
+ // All columns known from migrations, minus pk and timestamp cols (handled separately)
910
+ const tsColNames = [primaryKey, createdAtCol, updatedAtCol, deletedAtCol];
911
+ const allCols = new Set([...fillable, ...Object.keys(migrationColumns)]);
912
+
913
+ for (const col of allCols) {
914
+ if (tsColNames.includes(col)) continue;
915
+
916
+ // Cast takes priority over migration inference
917
+ if (casts[col]) {
918
+ const tsType = CAST_TYPE_MAP[casts[col]] || 'any';
919
+ fields.push(` ${col}?: ${tsType};`);
920
+ continue;
921
+ }
922
+
923
+ const migCol = migrationColumns[col];
924
+ if (migCol) {
925
+ const tsType = migCol.nullable ? `${migCol.type} | null` : migCol.type;
926
+ fields.push(` ${col}?: ${tsType};`);
927
+ } else {
928
+ fields.push(` ${col}?: any;`);
929
+ }
930
+ }
931
+
932
+ if (timestamps) {
933
+ fields.push(` ${createdAtCol}?: Date;`);
934
+ fields.push(` ${updatedAtCol}?: Date;`);
935
+ }
936
+
937
+ if (softDeletes) {
938
+ fields.push(` ${deletedAtCol}?: Date | null;`);
939
+ }
940
+
941
+ for (const { methodName, relationType, relatedModel } of relations) {
942
+ const returnType = RELATION_RETURN_MAP[relationType]?.(relatedModel) || 'any';
943
+ fields.push(` ${methodName}?: ${returnType};`);
944
+ }
945
+
946
+ const relationMethods = relations.map(({ methodName, relationType }) => {
947
+ const map = { hasOne: 'HasOne', hasMany: 'HasMany', belongsTo: 'BelongsTo', belongsToMany: 'BelongsToMany', hasManyThrough: 'HasManyThrough', morphTo: 'MorphTo', morphOne: 'MorphOne', morphMany: 'MorphMany' };
948
+ return ` ${methodName}(): ${map[relationType] || 'any'};`;
949
+ });
950
+
951
+ const usedRelTypes = [...new Set(relations.map(r => {
952
+ const map = { hasOne: 'HasOne', hasMany: 'HasMany', belongsTo: 'BelongsTo', belongsToMany: 'BelongsToMany', hasManyThrough: 'HasManyThrough', morphTo: 'MorphTo', morphOne: 'MorphOne', morphMany: 'MorphMany' };
953
+ return map[r.relationType];
954
+ }).filter(Boolean))];
955
+
956
+ const coreImports = usedRelTypes.length
957
+ ? `import { Model, ${usedRelTypes.join(', ')} } from 'ilana-orm';`
958
+ : `import { Model } from 'ilana-orm';`;
959
+
960
+ return `// Auto-generated by \`npx ilana types\` — do not edit manually
961
+ ${coreImports}
962
+ ${relatedModels.length ? `import type { ${relatedModels.join(', ')} } from './index';\n` : ''}
963
+ export interface ${className}Attributes {
964
+ ${fields.join('\n')}
965
+ }
966
+
967
+ declare class ${className} extends Model<${className}Attributes> {
968
+ ${relationMethods.length ? relationMethods.join('\n') + '\n' : ''}
969
+ }
970
+
971
+ export default ${className};
972
+ `;
973
+ }
974
+
975
+ async function generateTypes(outDir) {
976
+ // Only meaningful in TypeScript projects
977
+ if (!isTypeScriptProject()) return;
978
+
979
+ const structure = getProjectStructure();
980
+ const modelsDir = path.join(process.cwd(), structure.modelsDir);
981
+ const migrationsDir = path.join(process.cwd(), structure.databaseDir, 'migrations');
982
+
983
+ if (!fs.existsSync(modelsDir)) return;
984
+
985
+ const modelFiles = fs.readdirSync(modelsDir)
986
+ .filter(f => f.endsWith('.js') || f.endsWith('.ts'))
987
+ .filter(f => !f.endsWith('.d.ts'));
988
+
989
+ if (modelFiles.length === 0) return;
990
+
991
+ const outputDir = path.join(process.cwd(), outDir);
992
+ if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
993
+
994
+ const classNames = [];
995
+
996
+ for (const file of modelFiles) {
997
+ const content = fs.readFileSync(path.join(modelsDir, file), 'utf8');
998
+ const model = parseModelFile(content, file);
999
+ const migrationColumns = parseMigrationsForTable(model.table, migrationsDir);
1000
+ const dts = generateModelTypes(model, migrationColumns);
1001
+ const outFile = path.join(outputDir, `${model.className}.d.ts`);
1002
+ fs.writeFileSync(outFile, dts);
1003
+ console.log(` Generated: ${path.relative(process.cwd(), outFile)}`);
1004
+ classNames.push(model.className);
1005
+ }
1006
+
1007
+ const indexContent = classNames
1008
+ .map(n => `export { default as ${n}, type ${n}Attributes } from './${n}';`)
1009
+ .join('\n') + '\n';
1010
+ fs.writeFileSync(path.join(outputDir, 'index.d.ts'), `// Auto-generated by \`npx ilana types\` — do not edit manually\n${indexContent}`);
1011
+ console.log(` Generated: ${path.relative(process.cwd(), path.join(outputDir, 'index.d.ts'))}`);
1012
+ console.log(`\n✓ ${classNames.length} model type${classNames.length !== 1 ? 's' : ''} generated in ${outDir}/`);
1013
+ }
1014
+
1015
+ // ─── Commands ─────────────────────────────────────────────────────────────────
1016
+
752
1017
  const commands = {
753
1018
  async setup() {
754
1019
  console.log('Setting up Ilana ORM...');
@@ -920,6 +1185,7 @@ DB_TIMEZONE=UTC
920
1185
  }
921
1186
 
922
1187
  generateModel(name, options);
1188
+ await generateTypes('types');
923
1189
  },
924
1190
 
925
1191
  async migrate(...args) {
@@ -953,6 +1219,7 @@ DB_TIMEZONE=UTC
953
1219
  }
954
1220
 
955
1221
  await runner.migrate(connection, onlyFile, toFile);
1222
+ await generateTypes('types');
956
1223
  process.exit(0);
957
1224
  },
958
1225
 
@@ -979,6 +1246,7 @@ DB_TIMEZONE=UTC
979
1246
  await commands.seed();
980
1247
  }
981
1248
 
1249
+ await generateTypes('types');
982
1250
  process.exit(0);
983
1251
  },
984
1252
 
@@ -1043,6 +1311,7 @@ DB_TIMEZONE=UTC
1043
1311
  await initializeDatabase();
1044
1312
  const runner = new MigrationRunner();
1045
1313
  await runner.refresh(connection);
1314
+ await generateTypes('types');
1046
1315
  process.exit(0);
1047
1316
  },
1048
1317
 
@@ -1166,6 +1435,20 @@ DB_TIMEZONE=UTC
1166
1435
  }
1167
1436
  },
1168
1437
 
1438
+ async types(...args) {
1439
+ if (!isTypeScriptProject()) {
1440
+ console.log('Skipping type generation — not a TypeScript project.');
1441
+ return;
1442
+ }
1443
+ let outDir = 'types';
1444
+ for (const arg of args) {
1445
+ if (arg.startsWith('--out=')) outDir = arg.split('=')[1];
1446
+ else if (!arg.startsWith('--')) outDir = arg;
1447
+ }
1448
+ console.log('Generating model types...\n');
1449
+ await generateTypes(outDir);
1450
+ },
1451
+
1169
1452
  async 'make:cast'(name) {
1170
1453
  if (!name) {
1171
1454
  console.error('Cast name is required');
@@ -1220,7 +1503,10 @@ Available commands:
1220
1503
  seed [name] Run database seeders
1221
1504
  db:seed [name] Alias for seed command
1222
1505
  db:wipe [connection] Drop all tables
1223
-
1506
+
1507
+ types [--out=dir] Generate TypeScript types for all models
1508
+ Default output: types/
1509
+
1224
1510
  help Show this help message
1225
1511
 
1226
1512
  Examples:
@@ -1239,6 +1525,8 @@ Examples:
1239
1525
  ilana migrate:fresh --seed
1240
1526
  ilana seed UserSeeder
1241
1527
  ilana db:wipe
1528
+ ilana types
1529
+ ilana types --out=src/types
1242
1530
  `);
1243
1531
  }
1244
1532
  };
@@ -104,10 +104,9 @@ class SchemaBuilder {
104
104
 
105
105
  checkPositive(column) {
106
106
  const client = this.knex.client.config.client;
107
- if (client === 'pg') {
108
- return this.knex.raw(`ALTER TABLE ${this.currentTable} ADD CONSTRAINT ${column}_positive CHECK (${column} > 0)`);
109
- } else if (client === 'mysql2') {
110
- return this.knex.raw(`ALTER TABLE ${this.currentTable} ADD CONSTRAINT ${column}_positive CHECK (${column} > 0)`);
107
+ if (client === 'pg' || client === 'mysql2') {
108
+ return this.knex.raw(`ALTER TABLE ?? ADD CONSTRAINT ?? CHECK (?? > 0)`,
109
+ [this.currentTable, `${column}_positive`, column]);
111
110
  }
112
111
  return Promise.resolve();
113
112
  }
@@ -115,9 +114,11 @@ class SchemaBuilder {
115
114
  checkRegex(column, pattern) {
116
115
  const client = this.knex.client.config.client;
117
116
  if (client === 'pg') {
118
- return this.knex.raw(`ALTER TABLE ${this.currentTable} ADD CONSTRAINT ${column}_regex CHECK (${column} ~ '${pattern}')`);
117
+ return this.knex.raw(`ALTER TABLE ?? ADD CONSTRAINT ?? CHECK (?? ~ ?)`,
118
+ [this.currentTable, `${column}_regex`, column, pattern]);
119
119
  } else if (client === 'mysql2') {
120
- return this.knex.raw(`ALTER TABLE ${this.currentTable} ADD CONSTRAINT ${column}_regex CHECK (${column} REGEXP '${pattern}')`);
120
+ return this.knex.raw(`ALTER TABLE ?? ADD CONSTRAINT ?? CHECK (?? REGEXP ?)`,
121
+ [this.currentTable, `${column}_regex`, column, pattern]);
121
122
  }
122
123
  return Promise.resolve();
123
124
  }
@@ -125,9 +126,11 @@ class SchemaBuilder {
125
126
  generatedAs(column, expression) {
126
127
  const client = this.knex.client.config.client;
127
128
  if (client === 'mysql2') {
128
- return this.knex.raw(`ALTER TABLE ${this.currentTable} ADD ${column} VARCHAR(255) GENERATED ALWAYS AS (${expression}) STORED`);
129
+ return this.knex.raw(`ALTER TABLE ?? ADD ?? VARCHAR(255) GENERATED ALWAYS AS (${expression}) STORED`,
130
+ [this.currentTable, column]);
129
131
  } else if (client === 'pg') {
130
- return this.knex.raw(`ALTER TABLE ${this.currentTable} ADD ${column} TEXT GENERATED ALWAYS AS (${expression}) STORED`);
132
+ return this.knex.raw(`ALTER TABLE ?? ADD ?? TEXT GENERATED ALWAYS AS (${expression}) STORED`,
133
+ [this.currentTable, column]);
131
134
  }
132
135
  return Promise.resolve();
133
136
  }
@@ -135,9 +138,9 @@ class SchemaBuilder {
135
138
  collate(tableName, collation) {
136
139
  const client = this.knex.client.config.client;
137
140
  if (client === 'mysql2') {
138
- return this.knex.raw(`ALTER TABLE ${tableName} COLLATE ${collation}`);
141
+ return this.knex.raw(`ALTER TABLE ?? COLLATE ??`, [tableName, collation]);
139
142
  } else if (client === 'pg') {
140
- return this.knex.raw(`ALTER TABLE ${tableName} ALTER COLUMN name TYPE TEXT COLLATE "${collation}"`);
143
+ return this.knex.raw(`ALTER TABLE ?? ALTER COLUMN name TYPE TEXT COLLATE ??`, [tableName, collation]);
141
144
  }
142
145
  return Promise.resolve();
143
146
  }
@@ -166,7 +169,9 @@ class SchemaBuilder {
166
169
  fulltext(columns, indexName) {
167
170
  if (this.knex.client.config.client === 'mysql2') {
168
171
  const name = indexName || `${columns.join('_')}_fulltext`;
169
- return this.knex.raw(`ALTER TABLE ${this.currentTable} ADD FULLTEXT INDEX ${name} (${columns.join(', ')})`);
172
+ const colRefs = columns.map(() => '??').join(', ');
173
+ return this.knex.raw(`ALTER TABLE ?? ADD FULLTEXT INDEX ?? (${colRefs})`,
174
+ [this.currentTable, name, ...columns]);
170
175
  }
171
176
  return Promise.resolve();
172
177
  }
@@ -174,7 +179,8 @@ class SchemaBuilder {
174
179
  spatial(column, indexName) {
175
180
  if (this.knex.client.config.client === 'mysql2') {
176
181
  const name = indexName || `${column}_spatial`;
177
- return this.knex.raw(`ALTER TABLE ${this.currentTable} ADD SPATIAL INDEX ${name} (${column})`);
182
+ return this.knex.raw(`ALTER TABLE ?? ADD SPATIAL INDEX ?? (??)`,
183
+ [this.currentTable, name, column]);
178
184
  }
179
185
  return Promise.resolve();
180
186
  }
package/index.js CHANGED
@@ -24,6 +24,7 @@ module.exports = {
24
24
  defineFactory: Factory.defineFactory,
25
25
 
26
26
  // Relationships
27
+ MorphOne: Relation.MorphOne,
27
28
  ...Relation,
28
29
 
29
30
  // Custom Casts
package/index.mjs CHANGED
@@ -22,6 +22,7 @@ export const {
22
22
  BelongsToMany,
23
23
  HasManyThrough,
24
24
  MorphTo,
25
+ MorphOne,
25
26
  MorphMany,
26
27
  MoneyCast,
27
28
  EncryptedCast,
package/orm/Factory.js CHANGED
@@ -7,14 +7,14 @@ class Factory {
7
7
  this.definition = definition;
8
8
  this.faker = faker;
9
9
  this.states = new Map();
10
- this.afterCreating = [];
11
- this.afterMaking = [];
12
- this.beforeCreating = [];
13
- this.beforeMaking = [];
10
+ this._afterCreatingCallbacks = [];
11
+ this._afterMakingCallbacks = [];
12
+ this._beforeCreatingCallbacks = [];
13
+ this._beforeMakingCallbacks = [];
14
14
  this.count = 1;
15
15
  this.currentStates = [];
16
16
  this.relationships = new Map();
17
- this.sequence = 0;
17
+ this._sequenceCount = 0;
18
18
  this.sequences = new Map();
19
19
  }
20
20
 
@@ -28,22 +28,22 @@ class Factory {
28
28
  }
29
29
 
30
30
  afterCreating(callback) {
31
- this.afterCreating.push(callback);
31
+ this._afterCreatingCallbacks.push(callback);
32
32
  return this;
33
33
  }
34
34
 
35
35
  afterMaking(callback) {
36
- this.afterMaking.push(callback);
36
+ this._afterMakingCallbacks.push(callback);
37
37
  return this;
38
38
  }
39
39
 
40
40
  beforeCreating(callback) {
41
- this.beforeCreating.push(callback);
41
+ this._beforeCreatingCallbacks.push(callback);
42
42
  return this;
43
43
  }
44
44
 
45
45
  beforeMaking(callback) {
46
- this.beforeMaking.push(callback);
46
+ this._beforeMakingCallbacks.push(callback);
47
47
  return this;
48
48
  }
49
49
 
@@ -75,7 +75,7 @@ class Factory {
75
75
  }
76
76
 
77
77
  sequence() {
78
- return ++this.sequence;
78
+ return ++this._sequenceCount;
79
79
  }
80
80
 
81
81
  // Enhanced sequence methods
@@ -92,7 +92,7 @@ class Factory {
92
92
  this.sequences?.set(key, 0);
93
93
  } else {
94
94
  this.sequences?.clear();
95
- this.sequence = 0;
95
+ this._sequenceCount = 0;
96
96
  }
97
97
  return this;
98
98
  }
@@ -138,8 +138,6 @@ class Factory {
138
138
 
139
139
  if (typeof this.definition === 'function') {
140
140
  modelAttributes = this.definition(faker);
141
- } else if (this.definition === undefined && typeof this.definition === 'function') {
142
- modelAttributes = this.definition();
143
141
  } else {
144
142
  throw new Error('Factory must have a definition function');
145
143
  }
@@ -153,7 +151,7 @@ class Factory {
153
151
 
154
152
  modelAttributes = { ...modelAttributes, ...attributes };
155
153
 
156
- for (const callback of this.beforeMaking) {
154
+ for (const callback of this._beforeMakingCallbacks) {
157
155
  modelAttributes = callback(modelAttributes) || modelAttributes;
158
156
  }
159
157
 
@@ -167,7 +165,7 @@ class Factory {
167
165
  model.fill(modelAttributes);
168
166
 
169
167
  // Run afterMaking callbacks
170
- for (const callback of this.afterMaking) {
168
+ for (const callback of this._afterMakingCallbacks) {
171
169
  await callback(model);
172
170
  }
173
171
 
@@ -178,14 +176,14 @@ class Factory {
178
176
  const model = await this.makeOne(attributes);
179
177
 
180
178
  // Run beforeCreating callbacks
181
- for (const callback of this.beforeCreating) {
179
+ for (const callback of this._beforeCreatingCallbacks) {
182
180
  await callback(model);
183
181
  }
184
-
182
+
185
183
  await model.save();
186
184
 
187
185
  // Run afterCreating callbacks
188
- for (const callback of this.afterCreating) {
186
+ for (const callback of this._afterCreatingCallbacks) {
189
187
  await callback(model);
190
188
  }
191
189
 
@@ -243,6 +241,8 @@ class Factory {
243
241
  const batchFactory = new Factory(this.model, this.definition);
244
242
  batchFactory.count = currentBatchSize;
245
243
  batchFactory.currentStates = [...this.currentStates];
244
+ batchFactory._afterCreatingCallbacks = [...this._afterCreatingCallbacks];
245
+ batchFactory._beforeCreatingCallbacks = [...this._beforeCreatingCallbacks];
246
246
 
247
247
  const batch = await batchFactory.create(attributes);
248
248
  results.push(...(Array.isArray(batch) ? batch : [batch]));
@@ -352,6 +352,8 @@ if (typeof Model !== 'undefined') {
352
352
  // Return a new instance to avoid state pollution
353
353
  const newFactory = new Factory(this, existingFactory.definition);
354
354
  newFactory.states = new Map(existingFactory.states);
355
+ newFactory._afterCreatingCallbacks = [...existingFactory._afterCreatingCallbacks];
356
+ newFactory._beforeCreatingCallbacks = [...existingFactory._beforeCreatingCallbacks];
355
357
  return newFactory;
356
358
  }
357
359
  throw new Error(`No factory defined for model: ${this.name}`);
package/orm/Model.d.ts CHANGED
@@ -1,12 +1,17 @@
1
1
  import QueryBuilder from './QueryBuilder';
2
- import { HasOne, HasMany, BelongsTo, BelongsToMany, HasManyThrough, MorphTo, MorphMany } from './Relation';
2
+ import { HasOne, HasMany, BelongsTo, BelongsToMany, HasManyThrough, MorphTo, MorphOne, MorphMany } from './Relation';
3
3
 
4
4
  export interface ModelAttributes {
5
5
  [key: string]: any;
6
6
  }
7
7
 
8
+ export interface CastInstance {
9
+ get(value: any): any;
10
+ set(value: any): any;
11
+ }
12
+
8
13
  export interface ModelCasts {
9
- [key: string]: 'string' | 'number' | 'boolean' | 'date' | 'json' | 'array' | 'object' | 'float';
14
+ [key: string]: 'string' | 'number' | 'boolean' | 'date' | 'json' | 'array' | 'object' | 'float' | CastInstance;
10
15
  }
11
16
 
12
17
  export interface ModelEvents {
@@ -26,7 +31,7 @@ export interface Observer {
26
31
  restored?(model: any): Promise<void> | void;
27
32
  }
28
33
 
29
- export default class Model {
34
+ export default class Model<TAttributes extends ModelAttributes = ModelAttributes> {
30
35
  // Static properties
31
36
  protected static table: string;
32
37
  protected static connection?: string;
@@ -34,7 +39,10 @@ export default class Model {
34
39
  protected static keyType: 'number' | 'string';
35
40
  protected static incrementing: boolean;
36
41
  protected static timestamps: boolean;
42
+ protected static createdAt: string;
43
+ protected static updatedAt: string;
37
44
  protected static softDeletes: boolean;
45
+ protected static deletedAt: string;
38
46
  protected static fillable: string[];
39
47
  protected static guarded: string[];
40
48
  protected static casts: ModelCasts;
@@ -64,6 +72,7 @@ export default class Model {
64
72
  static resolveRelatedModel(related: string | typeof Model): typeof Model;
65
73
  static query(): QueryBuilder;
66
74
  static with(...relations: string[]): QueryBuilder;
75
+ static withCount(...relations: string[]): QueryBuilder;
67
76
  static on(connectionOrTrx: string | any): QueryBuilder;
68
77
  static all(): Promise<Model[]>;
69
78
  static find(id: any): Promise<Model | null>;
@@ -72,6 +81,10 @@ export default class Model {
72
81
  static firstOrFail(): Promise<Model>;
73
82
  static latest(column?: string): QueryBuilder;
74
83
  static oldest(column?: string): QueryBuilder;
84
+ static withTrashed(): QueryBuilder;
85
+ static onlyTrashed(): QueryBuilder;
86
+ static upsert(data: any[], uniqueBy: string[], update?: string[]): Promise<any>;
87
+ static withoutGlobalScopes(): QueryBuilder;
75
88
  static make(attributes?: ModelAttributes): Model;
76
89
  static create(attributes?: ModelAttributes): Promise<Model>;
77
90
  static generateUuid(): string;
@@ -111,10 +124,19 @@ export default class Model {
111
124
  // Instance methods
112
125
  getKey(): any;
113
126
  fill(attributes: ModelAttributes): this;
127
+ load(...relations: string[]): Promise<this>;
128
+ loadMissing(...relations: string[]): Promise<this>;
129
+ getRelation(key: string): any;
130
+ relationLoaded(key: string): boolean;
131
+ makeHidden(keys: string | string[]): this;
132
+ makeVisible(keys: string | string[]): this;
133
+ append(keys: string | string[]): this;
114
134
  isFillable(key: string): boolean;
115
135
  getAttribute(key: string): any;
116
136
  setAttribute(key: string, value: any): this;
117
137
  syncOriginal(): void;
138
+ getOriginal(key: string): any;
139
+ getOriginal(): ModelAttributes;
118
140
  save(): Promise<boolean>;
119
141
  update(attributes?: ModelAttributes): Promise<boolean>;
120
142
  isDirty(key?: string): boolean;
@@ -148,6 +170,7 @@ export default class Model {
148
170
  secondLocalKey?: string
149
171
  ): HasManyThrough;
150
172
  morphTo(typeColumn?: string, idColumn?: string): MorphTo;
173
+ morphOne(related: string | typeof Model, typeColumn?: string, idColumn?: string): MorphOne;
151
174
  morphMany(related: string | typeof Model, typeColumn?: string, idColumn?: string): MorphMany;
152
175
 
153
176
  // Protected methods