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/README.md +139 -108
- package/cli/ilana.js +289 -1
- package/database/schema-builder.js +18 -12
- package/index.js +1 -0
- package/index.mjs +1 -0
- package/orm/Factory.js +20 -18
- package/orm/Model.d.ts +26 -3
- package/orm/Model.js +138 -25
- package/orm/QueryBuilder.d.ts +24 -2
- package/orm/QueryBuilder.js +437 -75
- package/orm/Relation.d.ts +15 -3
- package/orm/Relation.js +74 -11
- package/orm/Relation.mjs +1 -1
- package/package.json +1 -1
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
|
|
109
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
141
|
+
return this.knex.raw(`ALTER TABLE ?? COLLATE ??`, [tableName, collation]);
|
|
139
142
|
} else if (client === 'pg') {
|
|
140
|
-
return this.knex.raw(`ALTER TABLE
|
|
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
|
-
|
|
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
|
|
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
package/index.mjs
CHANGED
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.
|
|
11
|
-
this.
|
|
12
|
-
this.
|
|
13
|
-
this.
|
|
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.
|
|
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.
|
|
31
|
+
this._afterCreatingCallbacks.push(callback);
|
|
32
32
|
return this;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
afterMaking(callback) {
|
|
36
|
-
this.
|
|
36
|
+
this._afterMakingCallbacks.push(callback);
|
|
37
37
|
return this;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
beforeCreating(callback) {
|
|
41
|
-
this.
|
|
41
|
+
this._beforeCreatingCallbacks.push(callback);
|
|
42
42
|
return this;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
beforeMaking(callback) {
|
|
46
|
-
this.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|