forge-sql-orm 1.0.15 → 1.0.16
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/package.json +3 -3
- package/scripts/actions/PatchPostinstall.ts +201 -0
- package/scripts/actions/generate-models.ts +65 -0
- package/scripts/actions/migrations-create.ts +193 -0
- package/scripts/actions/migrations-update.ts +200 -0
- package/scripts/cli.ts +221 -0
- package/{dist-cli → scripts}/forgeSqlCLI.js +1 -2
- package/dist-cli/cli.js +0 -571
- package/dist-cli/cli.js.map +0 -1
- package/dist-cli/cli.mjs +0 -570
- package/dist-cli/cli.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forge-sql-orm",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.16",
|
|
4
4
|
"description": "Mikro-ORM integration for Forge-SQL in Atlassian Forge applications.",
|
|
5
5
|
"main": "dist/ForgeSQLORM.js",
|
|
6
6
|
"module": "dist/ForgeSQLORM.mjs",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
},
|
|
75
75
|
"files": [
|
|
76
76
|
"dist",
|
|
77
|
-
"
|
|
77
|
+
"scripts",
|
|
78
78
|
"src",
|
|
79
79
|
"node_modules",
|
|
80
80
|
"tsconfig.json",
|
|
@@ -90,6 +90,6 @@
|
|
|
90
90
|
"@forge/sql": "^2.4.0"
|
|
91
91
|
},
|
|
92
92
|
"bin": {
|
|
93
|
-
"forge-sql-orm": "
|
|
93
|
+
"forge-sql-orm": "scripts/forgeSqlCLI.js"
|
|
94
94
|
}
|
|
95
95
|
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Automates patches for MikroORM and Knex to fix Webpack issues.
|
|
6
|
+
* - Removes problematic `require()` calls.
|
|
7
|
+
* - Deletes unnecessary files and folders.
|
|
8
|
+
* - Fixes dynamic imports (`import(id)`) in MikroORM.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
interface Patch {
|
|
12
|
+
file?: string; // File to modify (optional)
|
|
13
|
+
search?: RegExp; // Regex pattern to find problematic code
|
|
14
|
+
replace?: string; // Replacement string for problematic code
|
|
15
|
+
deleteLines?: RegExp[]; // List of regex patterns to remove specific lines
|
|
16
|
+
description: string; // Description of the patch
|
|
17
|
+
deleteFile?: string; // Path of the file to delete (optional)
|
|
18
|
+
deleteFolder?: string; // Path of the folder to delete (optional)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const PATCHES: Patch[] = [
|
|
22
|
+
// 🗑️ Remove unused dialects (mssql, postgres, sqlite) in MikroORM
|
|
23
|
+
{
|
|
24
|
+
file: "node_modules/@mikro-orm/knex/MonkeyPatchable.d.ts",
|
|
25
|
+
deleteLines: [/^.*mssql.*$/gim, /^.*MsSql.*$/gim, /^\s*Postgres.*$/gm, /^.*Sqlite3.*$/gm, /^.*BetterSqlite3.*$/gim],
|
|
26
|
+
description: "Removing unused dialects from MonkeyPatchable.d.ts"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
file: "node_modules/@mikro-orm/knex/MonkeyPatchable.js",
|
|
30
|
+
deleteLines: [/^.*mssql.*$/gim, /^.*MsSql.*$/gim, /^.*postgres.*$/gim, /^.*sqlite.*$/gim, /^.*Sqlite.*$/gim],
|
|
31
|
+
description: "Removing unused dialects from MonkeyPatchable.js"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
file: "node_modules/@mikro-orm/knex/dialects/index.js",
|
|
35
|
+
deleteLines: [/^.*mssql.*$/gim, /^.*MsSql.*$/gim, /^.*postgresql.*$/gim, /^.*sqlite.*$/gim],
|
|
36
|
+
description: "Removing unused dialects from @mikro-orm/knex/dialects/index.js"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
deleteFolder: "node_modules/@mikro-orm/knex/dialects/mssql",
|
|
40
|
+
description: "Removing mssql dialect from MikroORM"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
deleteFolder: "node_modules/@mikro-orm/knex/dialects/postgresql",
|
|
44
|
+
description: "Removing postgresql dialect from MikroORM"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
deleteFolder: "node_modules/@mikro-orm/knex/dialects/sqlite",
|
|
48
|
+
description: "Removing sqlite dialect from MikroORM"
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// 🔄 Fix Webpack `Critical dependency: the request of a dependency is an expression`
|
|
52
|
+
{
|
|
53
|
+
file: "node_modules/@mikro-orm/core/utils/Configuration.js",
|
|
54
|
+
search: /dynamicImportProvider:\s*\/\* istanbul ignore next \*\/\s*\(id\) => import\(id\),/g,
|
|
55
|
+
replace: "dynamicImportProvider: /* istanbul ignore next */ () => Promise.resolve({}),",
|
|
56
|
+
description: "Fixing dynamic imports in MikroORM Configuration"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
file: "node_modules/@mikro-orm/core/utils/Utils.js",
|
|
60
|
+
search: /static dynamicImportProvider = \(id\) => import\(id\);/g,
|
|
61
|
+
replace: "static dynamicImportProvider = () => Promise.resolve({});",
|
|
62
|
+
description: "Fixing dynamic imports in MikroORM Utils.js"
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// 🛑 Remove deprecated `require.extensions` usage
|
|
66
|
+
{
|
|
67
|
+
file: "node_modules/@mikro-orm/core/utils/Utils.js",
|
|
68
|
+
search: /\s\|\|\s*\(require\.extensions\s*&&\s*!!require\.extensions\['\.ts'\]\);\s*/g,
|
|
69
|
+
replace: ";",
|
|
70
|
+
description: "Removing deprecated `require.extensions` check in MikroORM"
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
// 🛠️ Patch Knex to remove `Migrator` and `Seeder`
|
|
74
|
+
{
|
|
75
|
+
file: "node_modules/knex/lib/knex-builder/make-knex.js",
|
|
76
|
+
deleteLines: [/^const \{ Migrator \} = require\('\.\.\/migrations\/migrate\/Migrator'\);$/gm, /^const Seeder = require\('\.\.\/migrations\/seed\/Seeder'\);$/gm],
|
|
77
|
+
description: "Removing `Migrator` and `Seeder` requires from make-knex.js"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
file: "node_modules/knex/lib/knex-builder/make-knex.js",
|
|
81
|
+
search: /\sreturn new Migrator\(this\);/g,
|
|
82
|
+
replace: "return null;",
|
|
83
|
+
description: "Replacing `return new Migrator(this);` with `return null;`"
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
file: "node_modules/knex/lib/knex-builder/make-knex.js",
|
|
87
|
+
search: /\sreturn new Seeder\(this\);/g,
|
|
88
|
+
replace: "return null;",
|
|
89
|
+
description: "Replacing `return new Seeder(this);` with `return null;`"
|
|
90
|
+
},
|
|
91
|
+
// 🔄 Patch for MikroORM Entity Generator to use 'forge-sql-orm'
|
|
92
|
+
// {
|
|
93
|
+
// file: "node_modules/@mikro-orm/entity-generator/SourceFile.js",
|
|
94
|
+
// search: /^.* from '@mikro-orm.*$/gim,
|
|
95
|
+
// replace: " }).join(', '))} } from 'forge-sql-orm';`);",
|
|
96
|
+
// description: "Replacing entity generator imports with 'forge-sql-orm'"
|
|
97
|
+
// },
|
|
98
|
+
{
|
|
99
|
+
file: "node_modules/knex/lib/dialects/index.js",
|
|
100
|
+
deleteLines: [/^.*mssql.*$/gim, /^.*MsSql.*$/gim, /^.*postgresql.*$/gim, /^.*sqlite.*$/gim, /^.*oracle.*$/gim, /^.*oracledb.*$/gim, /^.*pgnative.*$/gim, /^.*postgres.*$/gim, /^.*redshift.*$/gim, /^.*sqlite3.*$/gim, /^.*cockroachdb.*$/gim],
|
|
101
|
+
description: "Removing unused dialects from @mikro-orm/knex/dialects/index.js"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
file: "node_modules/@mikro-orm/core/utils/Utils.js",
|
|
105
|
+
search: /\s\|\|\s*\(require\.extensions\s*&&\s*!!require\.extensions\['\.ts'\]\);\s*/g,
|
|
106
|
+
replace: ";", // Replaces with semicolon to keep syntax valid
|
|
107
|
+
description: "Removing deprecated `require.extensions` check from MikroORM"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
file: "node_modules/@mikro-orm/core/utils/Utils.js",
|
|
111
|
+
search: /^.*extensions.*$/gim,
|
|
112
|
+
replace: "{", // Replaces with semicolon to keep syntax valid
|
|
113
|
+
description: "Removing deprecated `require.extensions` check from MikroORM"
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
file: "node_modules/@mikro-orm/core/utils/Utils.js",
|
|
117
|
+
search: /^.*package.json.*$/gim,
|
|
118
|
+
replace: "return 0;", // Replaces with semicolon to keep syntax valid
|
|
119
|
+
description: "Removing deprecated `require.extensions` check from MikroORM"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
file: "node_modules/@mikro-orm/knex/dialects/mysql/index.js",
|
|
123
|
+
deleteLines: [/^.*MariaDbKnexDialect.*$/gim],
|
|
124
|
+
description: "Removing MariaDbKnexDialect"
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Runs the MikroORM & Knex patching logic.
|
|
135
|
+
*/
|
|
136
|
+
export function runPostInstallPatch() {
|
|
137
|
+
console.log("🔧 Applying MikroORM & Knex patches...");
|
|
138
|
+
PATCHES.forEach(({ file, search, replace, deleteLines, deleteFile, deleteFolder, description }) => {
|
|
139
|
+
if (file) {
|
|
140
|
+
const filePath = path.resolve(file);
|
|
141
|
+
if (fs.existsSync(filePath)) {
|
|
142
|
+
let content = fs.readFileSync(filePath, "utf8");
|
|
143
|
+
let originalContent = content;
|
|
144
|
+
|
|
145
|
+
// 🔄 Replace text
|
|
146
|
+
if (search && replace) {
|
|
147
|
+
if (typeof search === "string" ? content.includes(search) : search.test(content)) {
|
|
148
|
+
content = content.replace(search, replace);
|
|
149
|
+
console.log(`[PATCHED] ${description}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 🗑️ Remove matching lines
|
|
154
|
+
if (deleteLines) {
|
|
155
|
+
deleteLines.forEach((pattern) => {
|
|
156
|
+
content = content
|
|
157
|
+
.split("\n")
|
|
158
|
+
.filter((line) => !pattern.test(line))
|
|
159
|
+
.join("\n");
|
|
160
|
+
});
|
|
161
|
+
if (content !== originalContent) {
|
|
162
|
+
console.log(`[CLEANED] Removed matching lines in ${file}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 💾 Save changes only if file was modified
|
|
167
|
+
if (content !== originalContent) {
|
|
168
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 🚮 Remove empty files
|
|
172
|
+
if (content.trim() === "") {
|
|
173
|
+
fs.unlinkSync(filePath);
|
|
174
|
+
console.log(`[REMOVED] ${filePath} (file is now empty)`);
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
console.warn(`[WARNING] File not found: ${file}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 🚮 Delete specific files
|
|
182
|
+
if (deleteFile) {
|
|
183
|
+
const deleteFilePath = path.resolve(__dirname, "../", deleteFile);
|
|
184
|
+
if (fs.existsSync(deleteFilePath)) {
|
|
185
|
+
fs.unlinkSync(deleteFilePath);
|
|
186
|
+
console.log(`[DELETED] ${description}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 🚮 Delete entire folders
|
|
191
|
+
if (deleteFolder) {
|
|
192
|
+
const deleteFolderPath = path.resolve(__dirname, "../", deleteFolder);
|
|
193
|
+
if (fs.existsSync(deleteFolderPath)) {
|
|
194
|
+
fs.rmSync(deleteFolderPath, {recursive: true, force: true});
|
|
195
|
+
console.log(`[DELETED] ${description}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
console.log("🎉 MikroORM & Knex patching completed!");
|
|
201
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import {defineConfig, MikroORM, MongoNamingStrategy} from "@mikro-orm/mysql";
|
|
5
|
+
import {EntityGenerator} from "@mikro-orm/entity-generator";
|
|
6
|
+
|
|
7
|
+
const regenerateIndexFile = (outputPath: string) => {
|
|
8
|
+
const entitiesDir = path.resolve(outputPath);
|
|
9
|
+
const indexPath = path.join(entitiesDir, "index.ts");
|
|
10
|
+
|
|
11
|
+
const entityFiles = fs
|
|
12
|
+
.readdirSync(entitiesDir)
|
|
13
|
+
.filter((file) => file.endsWith(".ts") && file !== "index.ts");
|
|
14
|
+
|
|
15
|
+
const imports = entityFiles.map((file) => {
|
|
16
|
+
const entityName = path.basename(file, ".ts");
|
|
17
|
+
return `import { ${entityName} } from "./${entityName}";`;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const indexContent = `${imports.join("\n")}\n\nexport default [${entityFiles.map((file) => path.basename(file, ".ts")).join(", ")}];\n`;
|
|
21
|
+
|
|
22
|
+
fs.writeFileSync(indexPath, indexContent, "utf8");
|
|
23
|
+
console.log(`✅ Updated index.ts with ${entityFiles.length} entities.`);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const generateModels = async (options: any) => {
|
|
27
|
+
try {
|
|
28
|
+
const ormConfig = defineConfig({
|
|
29
|
+
host: options.host,
|
|
30
|
+
port: options.port,
|
|
31
|
+
user: options.user,
|
|
32
|
+
password: options.password,
|
|
33
|
+
dbName: options.dbName,
|
|
34
|
+
namingStrategy: MongoNamingStrategy,
|
|
35
|
+
discovery: { warnWhenNoEntities: false },
|
|
36
|
+
extensions: [EntityGenerator],
|
|
37
|
+
debug: true,
|
|
38
|
+
}) as Parameters<typeof MikroORM.initSync>[0];
|
|
39
|
+
|
|
40
|
+
const orm = MikroORM.initSync(ormConfig);
|
|
41
|
+
console.log(`✅ Connected to ${options.dbName} at ${options.host}:${options.port}`);
|
|
42
|
+
|
|
43
|
+
await orm.entityGenerator.generate({
|
|
44
|
+
entitySchema: true,
|
|
45
|
+
bidirectionalRelations: false,
|
|
46
|
+
identifiedReferences: false,
|
|
47
|
+
forceUndefined: true,
|
|
48
|
+
undefinedDefaults: true,
|
|
49
|
+
useCoreBaseEntity: false,
|
|
50
|
+
onlyPurePivotTables: false,
|
|
51
|
+
outputPurePivotTables: false,
|
|
52
|
+
scalarPropertiesForRelations: "always",
|
|
53
|
+
save: true,
|
|
54
|
+
path: options.output,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
regenerateIndexFile(options.output);
|
|
58
|
+
|
|
59
|
+
console.log(`✅ Entities generated at: ${options.output}`);
|
|
60
|
+
process.exit(0);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error(`❌ Error generating entities:`, error);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import {MikroORM} from "@mikro-orm/mysql";
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import { rmSync } from 'fs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Cleans SQL statements by removing unnecessary database options.
|
|
10
|
+
* @param sql - The raw SQL statement.
|
|
11
|
+
* @returns The cleaned SQL statement.
|
|
12
|
+
*/
|
|
13
|
+
function cleanSQLStatement(sql: string): string {
|
|
14
|
+
return sql.replace(/\s+default\s+character\s+set\s+utf8mb4\s+engine\s*=\s*InnoDB;?/gi, "").trim();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generates a migration file using the provided SQL statements.
|
|
19
|
+
* @param createStatements - Array of SQL statements.
|
|
20
|
+
* @param version - Migration version number.
|
|
21
|
+
* @returns TypeScript migration file content.
|
|
22
|
+
*/
|
|
23
|
+
function generateMigrationFile(createStatements: string[], version: number): string {
|
|
24
|
+
const versionPrefix = `v${version}_MIGRATION`;
|
|
25
|
+
|
|
26
|
+
// Clean each SQL statement and generate migration lines with .enqueue()
|
|
27
|
+
const migrationLines = createStatements
|
|
28
|
+
.map(
|
|
29
|
+
(stmt, index) =>
|
|
30
|
+
` .enqueue("${versionPrefix}${index}", \"${cleanSQLStatement(stmt)}\")`, // eslint-disable-line no-useless-escape
|
|
31
|
+
)
|
|
32
|
+
.join("\n");
|
|
33
|
+
|
|
34
|
+
// Migration template
|
|
35
|
+
return `import { MigrationRunner } from "@forge/sql/out/migration";
|
|
36
|
+
|
|
37
|
+
export default (migrationRunner: MigrationRunner): MigrationRunner => {
|
|
38
|
+
return migrationRunner
|
|
39
|
+
${migrationLines};
|
|
40
|
+
};`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Saves the generated migration file along with `migrationCount.ts` and `index.ts`.
|
|
45
|
+
* @param migrationCode - The migration code to be written to the file.
|
|
46
|
+
* @param version - Migration version number.
|
|
47
|
+
* @param outputDir - Directory where the migration files will be saved.
|
|
48
|
+
*/
|
|
49
|
+
function saveMigrationFiles(migrationCode: string, version: number, outputDir: string) {
|
|
50
|
+
if (!fs.existsSync(outputDir)) {
|
|
51
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const migrationFilePath = path.join(outputDir, `migrationV${version}.ts`);
|
|
55
|
+
const migrationCountPath = path.join(outputDir, `migrationCount.ts`);
|
|
56
|
+
const indexFilePath = path.join(outputDir, `index.ts`);
|
|
57
|
+
|
|
58
|
+
// Write the migration file
|
|
59
|
+
fs.writeFileSync(migrationFilePath, migrationCode);
|
|
60
|
+
|
|
61
|
+
// Write the migration count file
|
|
62
|
+
fs.writeFileSync(migrationCountPath, `export const MIGRATION_VERSION = ${version};`);
|
|
63
|
+
|
|
64
|
+
// Generate the migration index file
|
|
65
|
+
const indexFileContent = `import { MigrationRunner } from "@forge/sql/out/migration";
|
|
66
|
+
import { MIGRATION_VERSION } from "./migrationCount";
|
|
67
|
+
|
|
68
|
+
export type MigrationType = (
|
|
69
|
+
migrationRunner: MigrationRunner,
|
|
70
|
+
) => MigrationRunner;
|
|
71
|
+
|
|
72
|
+
export default async (
|
|
73
|
+
migrationRunner: MigrationRunner,
|
|
74
|
+
): Promise<MigrationRunner> => {
|
|
75
|
+
for (let i = 1; i <= MIGRATION_VERSION; i++) {
|
|
76
|
+
const migrations = (await import(\`./migrationV\${i}\`)) as {
|
|
77
|
+
default: MigrationType;
|
|
78
|
+
};
|
|
79
|
+
migrations.default(migrationRunner);
|
|
80
|
+
}
|
|
81
|
+
return migrationRunner;
|
|
82
|
+
};`;
|
|
83
|
+
|
|
84
|
+
fs.writeFileSync(indexFilePath, indexFileContent);
|
|
85
|
+
|
|
86
|
+
console.log(`✅ Migration file created: ${migrationFilePath}`);
|
|
87
|
+
console.log(`✅ Migration count file updated: ${migrationCountPath}`);
|
|
88
|
+
console.log(`✅ Migration index file created: ${indexFilePath}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Extracts only the relevant SQL statements for migration.
|
|
93
|
+
* @param schema - The full database schema as SQL.
|
|
94
|
+
* @returns Filtered list of SQL statements.
|
|
95
|
+
*/
|
|
96
|
+
const extractCreateStatements = (schema: string): string[] => {
|
|
97
|
+
const statements = schema.split(";").map((s) => s.trim());
|
|
98
|
+
|
|
99
|
+
return statements.filter(
|
|
100
|
+
(stmt) =>
|
|
101
|
+
stmt.startsWith("create table") ||
|
|
102
|
+
(stmt.startsWith("alter table") && stmt.includes("add index")) ||
|
|
103
|
+
stmt.startsWith("primary"),
|
|
104
|
+
);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Dynamically loads `entities` from `index.ts` in the specified directory.
|
|
109
|
+
* @param entitiesPath - Path to the directory containing `index.ts`.
|
|
110
|
+
* @returns Array of entity classes.
|
|
111
|
+
*/
|
|
112
|
+
const loadEntities = async (entitiesPath: string) => {
|
|
113
|
+
try {
|
|
114
|
+
const indexFilePath = path.resolve(path.join(entitiesPath, "index.ts"));
|
|
115
|
+
if (!fs.existsSync(indexFilePath)) {
|
|
116
|
+
console.error(`❌ Error: index.ts not found in ${indexFilePath}`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const { default: entities } = await import(indexFilePath);
|
|
121
|
+
console.log(`✅ Loaded ${entities.length} entities from ${entitiesPath}`);
|
|
122
|
+
return entities;
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error(`❌ Error loading index.ts from ${entitiesPath}:`, error);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Loads the current migration version from `migrationCount.ts`.
|
|
131
|
+
* @param migrationPath - Path to the migration folder.
|
|
132
|
+
* @returns The latest migration version.
|
|
133
|
+
*/
|
|
134
|
+
const loadMigrationVersion = async (migrationPath: string): Promise<number> => {
|
|
135
|
+
try {
|
|
136
|
+
const migrationCountFilePath = path.resolve(path.join(migrationPath, "migrationCount.ts"));
|
|
137
|
+
if (!fs.existsSync(migrationCountFilePath)) {
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const { MIGRATION_VERSION } = await import(migrationCountFilePath);
|
|
142
|
+
console.log(`✅ Current migration version: ${MIGRATION_VERSION}`);
|
|
143
|
+
return MIGRATION_VERSION as number;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error(`❌ Error loading migrationCount:`, error);
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Creates a full database migration.
|
|
152
|
+
* @param options - Database connection settings and output paths.
|
|
153
|
+
*/
|
|
154
|
+
export const createMigration = async (options: any) => {
|
|
155
|
+
try {
|
|
156
|
+
let version = await loadMigrationVersion(options.output);
|
|
157
|
+
|
|
158
|
+
if (version > 0) {
|
|
159
|
+
console.error(`❌ Error: Migration has already been created.`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Start from version 1 if no previous migrations exist
|
|
164
|
+
version = 1;
|
|
165
|
+
|
|
166
|
+
// Load entities dynamically from index.ts
|
|
167
|
+
const entities = await loadEntities(options.entitiesPath);
|
|
168
|
+
|
|
169
|
+
// Initialize MikroORM
|
|
170
|
+
const orm = MikroORM.initSync({
|
|
171
|
+
host: options.host,
|
|
172
|
+
port: options.port,
|
|
173
|
+
user: options.user,
|
|
174
|
+
password: options.password,
|
|
175
|
+
dbName: options.dbName,
|
|
176
|
+
entities: entities,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Generate SQL schema
|
|
180
|
+
const createSchemaSQL = await orm.schema.getCreateSchemaSQL({ wrap: true });
|
|
181
|
+
const statements = extractCreateStatements(createSchemaSQL);
|
|
182
|
+
|
|
183
|
+
// Generate and save migration files
|
|
184
|
+
const migrationFile = generateMigrationFile(statements, version);
|
|
185
|
+
saveMigrationFiles(migrationFile, version, options.output);
|
|
186
|
+
|
|
187
|
+
console.log(`✅ Migration successfully created!`);
|
|
188
|
+
process.exit(0);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.error(`❌ Error during migration creation:`, error);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import "reflect-metadata";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import {MikroORM} from "@mikro-orm/mysql";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Cleans SQL statements by removing unnecessary database options.
|
|
8
|
+
* @param sql - The raw SQL statement.
|
|
9
|
+
* @returns The cleaned SQL statement.
|
|
10
|
+
*/
|
|
11
|
+
function cleanSQLStatement(sql: string): string {
|
|
12
|
+
return sql.replace(/\s+default\s+character\s+set\s+utf8mb4\s+engine\s*=\s*InnoDB;?/gi, "").trim();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generates a migration file using the provided SQL statements.
|
|
17
|
+
* @param createStatements - Array of SQL statements.
|
|
18
|
+
* @param version - Migration version number.
|
|
19
|
+
* @returns TypeScript migration file content.
|
|
20
|
+
*/
|
|
21
|
+
function generateMigrationFile(createStatements: string[], version: number): string {
|
|
22
|
+
const versionPrefix = `v${version}_MIGRATION`;
|
|
23
|
+
|
|
24
|
+
// Clean each SQL statement and generate migration lines with .enqueue()
|
|
25
|
+
const migrationLines = createStatements
|
|
26
|
+
.map(
|
|
27
|
+
(stmt, index) =>
|
|
28
|
+
` .enqueue("${versionPrefix}${index}", \"${cleanSQLStatement(stmt)}\")`, // eslint-disable-line no-useless-escape
|
|
29
|
+
)
|
|
30
|
+
.join("\n");
|
|
31
|
+
|
|
32
|
+
// Migration template
|
|
33
|
+
return `import { MigrationRunner } from "@forge/sql/out/migration";
|
|
34
|
+
|
|
35
|
+
export default (migrationRunner: MigrationRunner): MigrationRunner => {
|
|
36
|
+
return migrationRunner
|
|
37
|
+
${migrationLines};
|
|
38
|
+
};`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Saves the generated migration file along with `migrationCount.ts` and `index.ts`.
|
|
43
|
+
* @param migrationCode - The migration code to be written to the file.
|
|
44
|
+
* @param version - Migration version number.
|
|
45
|
+
* @param outputDir - Directory where the migration files will be saved.
|
|
46
|
+
*/
|
|
47
|
+
function saveMigrationFiles(migrationCode: string, version: number, outputDir: string) {
|
|
48
|
+
if (!fs.existsSync(outputDir)) {
|
|
49
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const migrationFilePath = path.join(outputDir, `migrationV${version}.ts`);
|
|
53
|
+
const migrationCountPath = path.join(outputDir, `migrationCount.ts`);
|
|
54
|
+
const indexFilePath = path.join(outputDir, `index.ts`);
|
|
55
|
+
|
|
56
|
+
// Write the migration file
|
|
57
|
+
fs.writeFileSync(migrationFilePath, migrationCode);
|
|
58
|
+
|
|
59
|
+
// Write the migration count file
|
|
60
|
+
fs.writeFileSync(migrationCountPath, `export const MIGRATION_VERSION = ${version};`);
|
|
61
|
+
|
|
62
|
+
// Generate the migration index file
|
|
63
|
+
const indexFileContent = `import { MigrationRunner } from "@forge/sql/out/migration";
|
|
64
|
+
import { MIGRATION_VERSION } from "./migrationCount";
|
|
65
|
+
|
|
66
|
+
export type MigrationType = (
|
|
67
|
+
migrationRunner: MigrationRunner,
|
|
68
|
+
) => MigrationRunner;
|
|
69
|
+
|
|
70
|
+
export default async (
|
|
71
|
+
migrationRunner: MigrationRunner,
|
|
72
|
+
): Promise<MigrationRunner> => {
|
|
73
|
+
for (let i = 1; i <= MIGRATION_VERSION; i++) {
|
|
74
|
+
const migrations = (await import(\`./migrationV\${i}\`)) as {
|
|
75
|
+
default: MigrationType;
|
|
76
|
+
};
|
|
77
|
+
migrations.default(migrationRunner);
|
|
78
|
+
}
|
|
79
|
+
return migrationRunner;
|
|
80
|
+
};`;
|
|
81
|
+
|
|
82
|
+
fs.writeFileSync(indexFilePath, indexFileContent);
|
|
83
|
+
|
|
84
|
+
console.log(`✅ Migration file created: ${migrationFilePath}`);
|
|
85
|
+
console.log(`✅ Migration count file updated: ${migrationCountPath}`);
|
|
86
|
+
console.log(`✅ Migration index file created: ${indexFilePath}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Extracts only the relevant SQL statements for migration.
|
|
91
|
+
* @param schema - The full database schema as SQL.
|
|
92
|
+
* @returns Filtered list of SQL statements.
|
|
93
|
+
*/
|
|
94
|
+
const extractCreateStatements = (schema: string): string[] => {
|
|
95
|
+
const statements = schema.split(";").map((s) => s.trim());
|
|
96
|
+
|
|
97
|
+
return statements.filter(
|
|
98
|
+
(stmt) =>
|
|
99
|
+
stmt.startsWith("create table") ||
|
|
100
|
+
(stmt.startsWith("alter table") && stmt.includes("add index")) ||
|
|
101
|
+
(stmt.startsWith("alter table") && stmt.includes("add") && !stmt.includes("foreign")) ||
|
|
102
|
+
(stmt.startsWith("alter table") && stmt.includes("modify") && !stmt.includes("foreign")),
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Dynamically loads `entities` from `index.ts` in the specified directory.
|
|
108
|
+
* @param entitiesPath - Path to the directory containing `index.ts`.
|
|
109
|
+
* @returns Array of entity classes.
|
|
110
|
+
*/
|
|
111
|
+
const loadEntities = async (entitiesPath: string) => {
|
|
112
|
+
try {
|
|
113
|
+
const indexFilePath = path.resolve(path.join(entitiesPath, "index.ts"));
|
|
114
|
+
if (!fs.existsSync(indexFilePath)) {
|
|
115
|
+
console.error(`❌ Error: index.ts not found in ${entitiesPath}`);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const { default: entities } = await import(indexFilePath);
|
|
120
|
+
console.log(`✅ Loaded ${entities.length} entities from ${entitiesPath}`);
|
|
121
|
+
return entities;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error(`❌ Error loading index.ts from ${entitiesPath}:`, error);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Loads the current migration version from `migrationCount.ts`.
|
|
130
|
+
* @param migrationPath - Path to the migration folder.
|
|
131
|
+
* @returns The latest migration version.
|
|
132
|
+
*/
|
|
133
|
+
const loadMigrationVersion = async (migrationPath: string): Promise<number> => {
|
|
134
|
+
try {
|
|
135
|
+
const migrationCountFilePath = path.resolve(path.join(migrationPath, "migrationCount.ts"));
|
|
136
|
+
if (!fs.existsSync(migrationCountFilePath)) {
|
|
137
|
+
console.warn(
|
|
138
|
+
`⚠️ Warning: migrationCount.ts not found in ${migrationCountFilePath}, assuming no previous migrations.`,
|
|
139
|
+
);
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const { MIGRATION_VERSION } = await import(migrationCountFilePath);
|
|
144
|
+
console.log(`✅ Current migration version: ${MIGRATION_VERSION}`);
|
|
145
|
+
return MIGRATION_VERSION as number;
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error(`❌ Error loading migrationCount:`, error);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Updates an existing database migration by generating schema modifications.
|
|
154
|
+
* @param options - Database connection settings and output paths.
|
|
155
|
+
*/
|
|
156
|
+
export const updateMigration = async (options: any) => {
|
|
157
|
+
try {
|
|
158
|
+
let version = await loadMigrationVersion(options.output);
|
|
159
|
+
|
|
160
|
+
if (version < 1) {
|
|
161
|
+
console.log(
|
|
162
|
+
`⚠️ Initial migration not found. Run "npx forge-sql-orm migrations:create" first.`,
|
|
163
|
+
);
|
|
164
|
+
process.exit(0);
|
|
165
|
+
}
|
|
166
|
+
version += 1;
|
|
167
|
+
|
|
168
|
+
// Load entities dynamically from index.ts
|
|
169
|
+
const entities = await loadEntities(options.entitiesPath);
|
|
170
|
+
|
|
171
|
+
// Initialize MikroORM
|
|
172
|
+
const orm = MikroORM.initSync({
|
|
173
|
+
host: options.host,
|
|
174
|
+
port: options.port,
|
|
175
|
+
user: options.user,
|
|
176
|
+
password: options.password,
|
|
177
|
+
dbName: options.dbName,
|
|
178
|
+
entities,
|
|
179
|
+
debug: true,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Generate SQL schema updates
|
|
183
|
+
const createSchemaSQL = await orm.schema.getUpdateSchemaMigrationSQL({ wrap: true });
|
|
184
|
+
const statements = extractCreateStatements(createSchemaSQL?.down || "");
|
|
185
|
+
|
|
186
|
+
if (statements.length) {
|
|
187
|
+
const migrationFile = generateMigrationFile(statements, version);
|
|
188
|
+
saveMigrationFiles(migrationFile, version, options.output);
|
|
189
|
+
|
|
190
|
+
console.log(`✅ Migration successfully updated!`);
|
|
191
|
+
process.exit(0);
|
|
192
|
+
} else {
|
|
193
|
+
console.log(`⚠️ No new migration changes detected.`);
|
|
194
|
+
process.exit(0);
|
|
195
|
+
}
|
|
196
|
+
} catch (error) {
|
|
197
|
+
console.error(`❌ Error during migration update:`, error);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
};
|