forge-sql-orm-cli 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,213 @@
1
+ import "reflect-metadata";
2
+ import fs from "fs";
3
+ import path from "path";
4
+
5
+ import { execSync } from "child_process";
6
+
7
+ /**
8
+ * Options for migration creation
9
+ */
10
+ export interface CreateMigrationOptions {
11
+ output: string;
12
+ entitiesPath: string;
13
+ force?: boolean;
14
+ }
15
+
16
+ /**
17
+ * Loads the current migration version from `migrationCount.ts`.
18
+ * @param migrationPath - Path to the migration folder.
19
+ * @returns The latest migration version.
20
+ */
21
+ export const loadMigrationVersion = async (migrationPath: string): Promise<number> => {
22
+ try {
23
+ const migrationCountFilePath = path.resolve(path.join(migrationPath, "migrationCount.ts"));
24
+ if (!fs.existsSync(migrationCountFilePath)) {
25
+ console.log(`✅ Current migration version: 0`);
26
+ return 0;
27
+ }
28
+
29
+ const { MIGRATION_VERSION } = await import(migrationCountFilePath);
30
+ console.log(`✅ Current migration version: ${MIGRATION_VERSION}`);
31
+ return MIGRATION_VERSION as number;
32
+ } catch (error) {
33
+ console.error(`❌ Error loading migrationCount:`, error);
34
+ process.exit(1);
35
+ }
36
+ };
37
+
38
+ /**
39
+ * Cleans SQL statements by removing unnecessary database options.
40
+ * @param sql - The raw SQL statement.
41
+ * @returns The cleaned SQL statement.
42
+ */
43
+ export function cleanSQLStatement(sql: string): string {
44
+ // Add IF NOT EXISTS to CREATE TABLE statements
45
+ sql = sql.replace(/create\s+table\s+(\w+)/gi, "create table if not exists $1");
46
+
47
+ // Add IF NOT EXISTS to CREATE INDEX statements
48
+ sql = sql.replace(/create\s+index\s+(\w+)/gi, "create index if not exists $1");
49
+
50
+ // Add IF NOT EXISTS to ADD INDEX statements
51
+ sql = sql.replace(
52
+ /alter\s+table\s+(\w+)\s+add\s+index\s+(\w+)/gi,
53
+ "alter table $1 add index if not exists $2",
54
+ );
55
+
56
+ // Add IF NOT EXISTS to ADD CONSTRAINT statements
57
+ sql = sql.replace(
58
+ /alter\s+table\s+(\w+)\s+add\s+constraint\s+(\w+)/gi,
59
+ "alter table $1 add constraint if not exists $2",
60
+ );
61
+
62
+ // Remove unnecessary database options
63
+ return sql.replace(/\s+default\s+character\s+set\s+utf8mb4\s+engine\s*=\s*InnoDB;?/gi, "").trim();
64
+ }
65
+
66
+ /**
67
+ * Generates a migration file using the provided SQL statements.
68
+ * @param createStatements - Array of SQL statements.
69
+ * @param version - Migration version number.
70
+ * @returns TypeScript migration file content.
71
+ */
72
+ export function generateMigrationFile(createStatements: string[], version: number): string {
73
+ const versionPrefix = `v${version}_MIGRATION`;
74
+
75
+ // Clean each SQL statement and generate migration lines with .enqueue()
76
+ const migrationLines = createStatements
77
+ .map(
78
+ (stmt, index) =>
79
+ ` .enqueue("${versionPrefix}${index}", "${cleanSQLStatement(stmt).replace(/\s+/g, " ")}")`,
80
+ )
81
+ .join("\n");
82
+
83
+ // Migration template
84
+ return `import { MigrationRunner } from "@forge/sql/out/migration";
85
+
86
+ export default (migrationRunner: MigrationRunner): MigrationRunner => {
87
+ return migrationRunner
88
+ ${migrationLines};
89
+ };`;
90
+ }
91
+
92
+ /**
93
+ * Saves the generated migration file along with `migrationCount.ts` and `index.ts`.
94
+ * @param migrationCode - The migration code to be written to the file.
95
+ * @param version - Migration version number.
96
+ * @param outputDir - Directory where the migration files will be saved.
97
+ */
98
+ export function saveMigrationFiles(migrationCode: string, version: number, outputDir: string) {
99
+ if (!fs.existsSync(outputDir)) {
100
+ fs.mkdirSync(outputDir, { recursive: true });
101
+ }
102
+
103
+ const migrationFilePath = path.join(outputDir, `migrationV${version}.ts`);
104
+ const migrationCountPath = path.join(outputDir, `migrationCount.ts`);
105
+ const indexFilePath = path.join(outputDir, `index.ts`);
106
+
107
+ // Write the migration file
108
+ fs.writeFileSync(migrationFilePath, migrationCode);
109
+
110
+ // Write the migration count file
111
+ fs.writeFileSync(migrationCountPath, `export const MIGRATION_VERSION = ${version};`);
112
+
113
+ // Generate the migration index file
114
+ const indexFileContent = `import { MigrationRunner } from "@forge/sql/out/migration";
115
+ import { MIGRATION_VERSION } from "./migrationCount";
116
+
117
+ export type MigrationType = (
118
+ migrationRunner: MigrationRunner,
119
+ ) => MigrationRunner;
120
+
121
+ export default async (
122
+ migrationRunner: MigrationRunner,
123
+ ): Promise<MigrationRunner> => {
124
+ for (let i = 1; i <= MIGRATION_VERSION; i++) {
125
+ const migrations = (await import(\`./migrationV\${i}\`)) as {
126
+ default: MigrationType;
127
+ };
128
+ migrations.default(migrationRunner);
129
+ }
130
+ return migrationRunner;
131
+ };`;
132
+
133
+ fs.writeFileSync(indexFilePath, indexFileContent);
134
+
135
+ console.log(`✅ Migration file created: ${migrationFilePath}`);
136
+ console.log(`✅ Migration count file updated: ${migrationCountPath}`);
137
+ console.log(`✅ Migration index file created: ${indexFilePath}`);
138
+ }
139
+
140
+ /**
141
+ * Extracts only the relevant SQL statements for migration.
142
+ * @param schema - The full database schema as SQL.
143
+ * @returns Filtered list of SQL statements.
144
+ */
145
+ export const extractCreateStatements = (schema: string): string[] => {
146
+ // Split by statement-breakpoint and semicolon
147
+ const statements = schema
148
+ .split(/--> statement-breakpoint|;/)
149
+ .map((s) => s.trim())
150
+ .filter((s) => s.length > 0);
151
+
152
+ return statements.filter(
153
+ (stmt) =>
154
+ stmt.toLowerCase().startsWith("create table") ||
155
+ stmt.toLowerCase().startsWith("alter table") ||
156
+ stmt.toLowerCase().includes("add index") ||
157
+ stmt.toLowerCase().includes("create index") ||
158
+ stmt.toLowerCase().includes("add unique index") ||
159
+ stmt.toLowerCase().includes("add constraint"),
160
+ );
161
+ };
162
+
163
+ /**
164
+ * Creates a full database migration.
165
+ * @param options - Database connection settings and output paths.
166
+ */
167
+ export const createMigration = async (options: CreateMigrationOptions) => {
168
+ try {
169
+ let version = await loadMigrationVersion(options.output);
170
+
171
+ if (version > 0) {
172
+ if (options.force) {
173
+ console.warn(
174
+ `⚠️ Warning: Migration already exists. Creating new migration with force flag...`,
175
+ );
176
+ } else {
177
+ console.error(
178
+ `❌ Error: Migration has already been created. Use --force flag to override.`,
179
+ );
180
+ process.exit(1);
181
+ }
182
+ }
183
+
184
+ // Start from version 1 if no previous migrations exist
185
+ version = 1;
186
+ // Generate SQL using drizzle-kit
187
+ await execSync(
188
+ `npx drizzle-kit generate --name=init --dialect mysql --out ${options.output} --schema ${options.entitiesPath}`,
189
+ { encoding: "utf-8" },
190
+ );
191
+ const initSqlFile = path.join(options.output, "0000_init.sql");
192
+ const sql = fs.readFileSync(initSqlFile, "utf-8");
193
+
194
+ // Extract and clean statements
195
+ const createStatements = extractCreateStatements(sql);
196
+
197
+ // Generate and save migration files
198
+ const migrationFile = generateMigrationFile(createStatements, 1);
199
+ saveMigrationFiles(migrationFile, 1, options.output);
200
+
201
+ fs.rmSync(initSqlFile, { force: true });
202
+ console.log(`✅ Removed SQL file: ${initSqlFile}`);
203
+ // Remove meta directory after processing
204
+ let metaDir = path.join(options.output, "meta");
205
+ fs.rmSync(metaDir, { recursive: true, force: true });
206
+ console.log(`✅ Removed: ${metaDir}`);
207
+ console.log(`✅ Migration successfully created!`);
208
+ process.exit(0);
209
+ } catch (error) {
210
+ console.error(`❌ Error during migration creation:`, error);
211
+ process.exit(1);
212
+ }
213
+ };
@@ -0,0 +1,139 @@
1
+ import "reflect-metadata";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { MySqlTable, TableConfig } from "drizzle-orm/mysql-core";
5
+ import { getTableMetadata, generateDropTableStatements } from "forge-sql-orm";
6
+
7
+ /**
8
+ * Generates a migration ID using current date
9
+ * @returns Migration ID string with current date
10
+ */
11
+ function generateMigrationUUID(version: number): string {
12
+ const now = new Date();
13
+ const timestamp = now.getTime();
14
+ return `MIGRATION_V${version}_${timestamp}`;
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 uniqId = generateMigrationUUID(version);
25
+ // Clean each SQL statement and generate migration lines with .enqueue()
26
+ const migrationLines = createStatements
27
+ .map(
28
+ (stmt, index) => ` .enqueue("${uniqId}_${index}", \"${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
+ * Creates a full database migration.
91
+ * @param options - Database connection settings and output paths.
92
+ */
93
+ export const dropMigration = async (options: any) => {
94
+ try {
95
+ // Start from version 1 if no previous migrations exist
96
+ const version = 1;
97
+
98
+ // Import Drizzle schema using absolute path
99
+ const schemaPath = path.resolve(options.entitiesPath, "schema.ts");
100
+ if (!fs.existsSync(schemaPath)) {
101
+ throw new Error(`Schema file not found at: ${schemaPath}`);
102
+ }
103
+
104
+ const schemaModule = await import(schemaPath);
105
+ if (!schemaModule) {
106
+ throw new Error(`Invalid schema file at: ${schemaPath}. Schema must export tables.`);
107
+ }
108
+
109
+ // Get all exports that are tables
110
+ const tables = Object.values(schemaModule) as MySqlTable<TableConfig>[];
111
+
112
+ if (tables.length === 0) {
113
+ throw new Error(`No valid tables found in schema at: ${schemaPath}`);
114
+ }
115
+
116
+ // Get table names for logging
117
+ const tableNames = tables
118
+ .map((table) => {
119
+ const metadata = getTableMetadata(table);
120
+ return metadata.tableName;
121
+ })
122
+ .filter(Boolean);
123
+
124
+ console.log("Found tables:", tableNames);
125
+
126
+ // Generate drop statements
127
+ const dropStatements = generateDropTableStatements(tables);
128
+
129
+ // Generate and save migration files
130
+ const migrationFile = generateMigrationFile(dropStatements, version);
131
+ saveMigrationFiles(migrationFile, version, options.output);
132
+
133
+ console.log(`✅ Migration successfully created!`);
134
+ process.exit(0);
135
+ } catch (error) {
136
+ console.error(`❌ Error during migration creation:`, error);
137
+ process.exit(1);
138
+ }
139
+ };