mango-orm 1.0.2 β†’ 1.1.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.
package/README.md CHANGED
@@ -900,7 +900,88 @@ Schema type builder for column definitions.
900
900
 
901
901
  ---
902
902
 
903
- ## πŸ” Security
903
+ ## οΏ½ Database Migrations
904
+
905
+ Mango includes a powerful migration system to version and manage database schema changes with color-coded console feedback.
906
+
907
+ ### Creating Migration Files
908
+
909
+ Generate a new migration file:
910
+
911
+ ```bash
912
+ npm run migration:generate create_users_table
913
+ ```
914
+
915
+ This creates a timestamped migration file like `migrations/1734912000000_create_users_table.ts`:
916
+
917
+ ```typescript
918
+ import { IMangoMigrationType, Mango } from "mango-orm";
919
+
920
+ export const create_users_table: IMangoMigrationType = {
921
+ name: "create_users_table",
922
+ timestamp: 1734912000000,
923
+
924
+ up: async (mango: Mango) => {
925
+ await mango.createTable("users", {
926
+ id: mango.types().int().primaryKey().autoIncrement(),
927
+ username: mango.types().varchar(255).notNull().unique(),
928
+ email: mango.types().varchar(255).notNull(),
929
+ created_at: mango.types().timeStamp().default("CURRENT_TIMESTAMP")
930
+ });
931
+ console.log("βœ“ Users table created");
932
+ },
933
+
934
+ down: async (mango: Mango) => {
935
+ await mango.dropTable("users");
936
+ console.log("βœ“ Users table dropped");
937
+ }
938
+ };
939
+ ```
940
+
941
+ ### Running Migrations
942
+
943
+ Create a migration runner script:
944
+
945
+ ```typescript
946
+ import { Mango, MangoMigration } from "mango-orm";
947
+ import { create_users_table } from "./migrations/1734912000000_create_users_table.js";
948
+
949
+ const mango = new Mango();
950
+ await mango.connect({ /* config */ });
951
+
952
+ const migration = new MangoMigration(mango);
953
+ migration.add(create_users_table);
954
+
955
+ // Check migration status
956
+ await migration.status();
957
+
958
+ // Run next pending migration
959
+ await migration.migrateUp();
960
+
961
+ // Run all pending migrations
962
+ await migration.migrateUpToLatest();
963
+
964
+ // Rollback last migration
965
+ await migration.migrateDown();
966
+
967
+ // Rollback all migrations
968
+ await migration.migrateDownToOldest();
969
+ ```
970
+
971
+ ### Migration Console Output
972
+
973
+ ```
974
+ === Migration Status ===
975
+
976
+ βœ“ Executed: create_users_table
977
+ β¦Ώ Pending: add_posts_table
978
+
979
+ Total: 2 | Executed: 1 | Pending: 1
980
+ ```
981
+
982
+ ---
983
+
984
+ ## οΏ½πŸ” Security
904
985
 
905
986
  Mango ORM is built with security in mind:
906
987
 
@@ -982,9 +1063,9 @@ await mango.connect({
982
1063
 
983
1064
  While Mango is functional, some features are still in development:
984
1065
 
1066
+ - βœ… **Migration System**: Database schema migrations with version tracking
985
1067
  - ⏳ **No Transaction Support**: BEGIN/COMMIT/ROLLBACK not fully implemented
986
1068
  - ⏳ **Limited JOIN Support**: Basic joins work but complex joins need testing
987
- - ⏳ **No Migration System**: Schema versioning and migrations coming soon
988
1069
  - ⏳ **No Relations**: ORM-style relations (hasMany, belongsTo) not yet available
989
1070
  - ⏳ **No Query Caching**: Results are not cached (may add in future)
990
1071
  - ⏳ **No Connection Retry**: Failed connections don't auto-retry
@@ -994,17 +1075,17 @@ While Mango is functional, some features are still in development:
994
1075
 
995
1076
  ## πŸ—ΊοΈ Roadmap
996
1077
 
997
- ### Short Term (v0.3.0)
1078
+ ### Short Term (v1.1.0)
998
1079
  - [x] WHERE clause support
999
1080
  - [x] UPDATE operations
1000
1081
  - [x] DELETE operations
1001
1082
  - [x] Comprehensive documentation
1083
+ - [x] Migration system with CLI
1002
1084
  - [ ] Transaction support (BEGIN, COMMIT, ROLLBACK)
1003
1085
  - [ ] Better error messages with stack traces
1004
1086
  - [ ] Connection pool configuration options
1005
1087
 
1006
- ### Medium Term (v0.4.0)
1007
- - [ ] Migration system
1088
+ ### Medium Term (v1.2.0)
1008
1089
  - [ ] Seed data functionality
1009
1090
  - [ ] Query result caching
1010
1091
  - [ ] Batch operation optimization
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+ import chalk from "chalk";
3
+ import { generateMangoMigrationFile } from "./migration-cli.js";
4
+ const command = process.argv[2];
5
+ const migrationName = process.argv[3];
6
+ const outputDir = process.argv[4] || './migrations';
7
+ console.log(chalk.bold.cyan('\nπŸ₯­ Mango Migration CLI\n'));
8
+ switch (command) {
9
+ case "generate":
10
+ case "g":
11
+ if (!migrationName) {
12
+ console.log(chalk.red("βœ—") + " Migration name is required\n");
13
+ showHelp();
14
+ process.exit(1);
15
+ }
16
+ generateMangoMigrationFile(migrationName, outputDir);
17
+ break;
18
+ case "help":
19
+ case "--help":
20
+ case "-h":
21
+ showHelp();
22
+ break;
23
+ default:
24
+ console.log(chalk.red("βœ—") + ` Unknown command: ${command}\n`);
25
+ showHelp();
26
+ process.exit(1);
27
+ }
28
+ function showHelp() {
29
+ console.log(chalk.bold("Usage:"));
30
+ console.log(" npm run migration:generate <name> [outputDir]\n");
31
+ console.log(chalk.bold("Commands:"));
32
+ console.log(chalk.cyan(" generate, g") + " Generate a new migration file");
33
+ console.log(chalk.cyan(" help, -h") + " Show this help message\n");
34
+ console.log(chalk.bold("Examples:"));
35
+ console.log(" npm run migration:generate create_users_table");
36
+ console.log(" npm run migration:generate add_email_column");
37
+ console.log(" npm run migration:generate create_posts_table ./db/migrations\n");
38
+ }
@@ -274,4 +274,29 @@ declare class Mango {
274
274
  haveTable(name: string): boolean;
275
275
  customQuery<Type>(query: string, supplies: any[]): Promise<Type>;
276
276
  }
277
+ interface IMangoMigrationType {
278
+ name: string;
279
+ timestamp: number;
280
+ up: (mango: Mango) => Promise<void>;
281
+ down: (mango: Mango) => Promise<void>;
282
+ }
283
+ declare class MangoMigration {
284
+ private mango;
285
+ private mango_migration_table_name;
286
+ private migrations;
287
+ initialize(): Promise<void>;
288
+ constructor(mango: Mango);
289
+ addOneMigrationToDB(migration: IMangoMigrationType): Promise<void>;
290
+ addManyMigrationToDB(migration: IMangoMigrationType[]): Promise<void>;
291
+ deleteOneMigrationFromDB(migration: IMangoMigrationType): Promise<void>;
292
+ deleteManyMigrationFromDB(migrations: IMangoMigrationType[]): Promise<void>;
293
+ add(migration: IMangoMigrationType): MangoMigration;
294
+ getExecutedMigrations(): Promise<string[]>;
295
+ migrateUp(): Promise<void>;
296
+ migrateUpToLatest(): Promise<void>;
297
+ migrateDown(): Promise<void>;
298
+ migrateDownToOldest(): Promise<void>;
299
+ status(): Promise<void>;
300
+ }
277
301
  export { Mango, MangoType, MangoTable };
302
+ export { MangoMigration, IMangoMigrationType };
package/dist/src/mango.js CHANGED
@@ -2,6 +2,7 @@
2
2
  Requires sql for connection
3
3
  */
4
4
  import * as sql from "mysql";
5
+ import chalk from "chalk";
5
6
  /**
6
7
  * MangoType - Schema builder for defining table column types
7
8
  * Provides chainable methods to build SQL column definitions
@@ -676,7 +677,7 @@ class Mango {
676
677
  return [...this.tables];
677
678
  }
678
679
  async createTable(name, fields) {
679
- this.query.query = "CREATE TABLE " + name + "( \n";
680
+ this.query.query = "CREATE TABLE " + name.toLowerCase() + "( \n";
680
681
  const fieldEnteries = Object.entries(fields);
681
682
  let table = new MangoTable(this.db, name.toLowerCase(), [
682
683
  ...fieldEnteries.map(([key, value], index) => {
@@ -690,6 +691,7 @@ class Mango {
690
691
  }
691
692
  });
692
693
  this.query.query += "\n)";
694
+ // console.log(this.query.query);
693
695
  await this.query.execute();
694
696
  this.query.query = "";
695
697
  this.tables.push(table);
@@ -719,4 +721,213 @@ class Mango {
719
721
  return this.query.customQuery(query, supplies);
720
722
  }
721
723
  }
724
+ class MangoMigration {
725
+ async initialize() {
726
+ try {
727
+ if (!this.mango.haveTable(this.mango_migration_table_name)) {
728
+ await this.mango.createTable(this.mango_migration_table_name, {
729
+ id: this.mango.types().int().primaryKey().notNull().autoIncrement(),
730
+ name: this.mango.types().text().notNull().unique(),
731
+ timestamp: this.mango.types().bigInt().notNull(),
732
+ executed_at: this.mango.types().bigInt().notNull(),
733
+ });
734
+ }
735
+ }
736
+ catch (error) {
737
+ console.log("Error encountered while initializing mangoMigration");
738
+ throw error;
739
+ }
740
+ }
741
+ constructor(mango) {
742
+ this.mango_migration_table_name = "mango_migrations";
743
+ this.migrations = [];
744
+ this.mango = mango;
745
+ this.initialize();
746
+ }
747
+ async addOneMigrationToDB(migration) {
748
+ try {
749
+ await this.mango.selectTable(this.mango_migration_table_name).insertOne({
750
+ name: migration.name,
751
+ timestamp: migration.timestamp,
752
+ executed_at: Date.now()
753
+ }).execute();
754
+ console.log(chalk.dim(` β†’ Recorded in database`));
755
+ }
756
+ catch (error) {
757
+ console.error(chalk.red(`βœ— Error:`) + ` Failed to record migration in DB:`, error.message);
758
+ throw error;
759
+ }
760
+ }
761
+ async addManyMigrationToDB(migration) {
762
+ try {
763
+ await this.mango.selectTable(this.mango_migration_table_name).insertMany(["name", "timestamp", "executed_at"], migration.map((m) => ([
764
+ m.name,
765
+ m.timestamp,
766
+ Date.now()
767
+ ]))).execute();
768
+ console.log(chalk.dim(` β†’ Recorded ${migration.length} migrations in database`));
769
+ }
770
+ catch (error) {
771
+ console.error(chalk.red(`βœ— Error:`) + ` Failed to record migrations in DB:`, error.message);
772
+ throw error;
773
+ }
774
+ }
775
+ async deleteOneMigrationFromDB(migration) {
776
+ try {
777
+ await this.mango.selectTable(this.mango_migration_table_name).delete().where("name", "=", migration.name).execute();
778
+ console.log(chalk.dim(` β†’ Removed from database`));
779
+ }
780
+ catch (error) {
781
+ console.error(chalk.red(`βœ— Error:`) + ` Failed to delete migration from DB:`, error);
782
+ throw error;
783
+ }
784
+ }
785
+ async deleteManyMigrationFromDB(migrations) {
786
+ const names = [...migrations].map((m) => m.name);
787
+ try {
788
+ await this.mango.selectTable(this.mango_migration_table_name).delete().whereIn("name", names).execute();
789
+ console.log(chalk.dim(` β†’ Removed ${migrations.length} migrations from database`));
790
+ }
791
+ catch (error) {
792
+ console.error(chalk.red(`βœ— Error:`) + ` Failed to delete migrations from DB:`, error.message);
793
+ throw error;
794
+ }
795
+ }
796
+ add(migration) {
797
+ this.migrations.push(migration);
798
+ return this;
799
+ }
800
+ async getExecutedMigrations() {
801
+ const migration = await this.mango.selectTable(this.mango_migration_table_name).selectAll().orderBy("timestamp").sort(1).execute();
802
+ return migration.map((m) => m.name);
803
+ }
804
+ async migrateUp() {
805
+ await this.initialize();
806
+ const executed = await this.getExecutedMigrations();
807
+ const sorted = [...this.migrations].sort((a, b) => a.timestamp - b.timestamp);
808
+ const pending = sorted.find(m => !executed.includes(m.name));
809
+ if (!pending) {
810
+ console.log(chalk.yellow("⚠") + " No pending migrations to execute");
811
+ return;
812
+ }
813
+ console.log(chalk.cyan("β†’") + ` Running migration: ${chalk.bold(pending.name)}`);
814
+ try {
815
+ await pending.up(this.mango);
816
+ console.log(chalk.green("βœ“") + ` Migration completed: ${chalk.bold(pending.name)}`);
817
+ await this.addOneMigrationToDB(pending);
818
+ }
819
+ catch (error) {
820
+ console.error(chalk.red("βœ—") + ` Migration failed: ${chalk.bold(pending.name)}`);
821
+ console.error(chalk.dim(` Error: ${error.message}`));
822
+ throw error;
823
+ }
824
+ }
825
+ async migrateUpToLatest() {
826
+ await this.initialize();
827
+ const executed = await this.getExecutedMigrations();
828
+ const sorted = [...this.migrations].sort((a, b) => a.timestamp - b.timestamp);
829
+ const pending = sorted.filter(m => !executed.includes(m.name));
830
+ if (pending.length === 0) {
831
+ console.log(chalk.yellow("⚠") + " No pending migrations to execute");
832
+ return;
833
+ }
834
+ console.log(chalk.cyan("β†’") + ` Running ${pending.length} pending migration(s)...\n`);
835
+ for (const migration of pending) {
836
+ console.log(chalk.cyan(" β†’") + ` ${chalk.bold(migration.name)}`);
837
+ try {
838
+ await migration.up(this.mango);
839
+ await this.addOneMigrationToDB(migration);
840
+ console.log(chalk.green(" βœ“") + ` Completed\n`);
841
+ }
842
+ catch (error) {
843
+ console.error(chalk.red(" βœ—") + ` Failed`);
844
+ console.error(chalk.dim(` Error: ${error.message}`));
845
+ throw error;
846
+ }
847
+ }
848
+ console.log(chalk.green("βœ“") + ` All migrations completed successfully`);
849
+ }
850
+ async migrateDown() {
851
+ await this.initialize();
852
+ const executed = await this.getExecutedMigrations();
853
+ const sorted = [...this.migrations].sort((a, b) => a.timestamp - b.timestamp);
854
+ if (executed.length === 0) {
855
+ console.log(chalk.yellow("⚠") + " No migrations to rollback");
856
+ return;
857
+ }
858
+ const oldMigrationName = executed[executed.length - 1];
859
+ const migration = sorted.find(m => m.name === oldMigrationName);
860
+ if (!migration) {
861
+ throw new Error(`Migration not found: ${oldMigrationName}`);
862
+ }
863
+ console.log(chalk.magenta("←") + ` Rolling back: ${chalk.bold(migration.name)}`);
864
+ try {
865
+ await migration.down(this.mango);
866
+ await this.deleteOneMigrationFromDB(migration);
867
+ console.log(chalk.green("βœ“") + ` Rollback completed`);
868
+ }
869
+ catch (error) {
870
+ console.error(chalk.red("βœ—") + ` Rollback failed: ${chalk.bold(migration.name)}`);
871
+ console.error(chalk.dim(` Error: ${error.message}`));
872
+ throw error;
873
+ }
874
+ return;
875
+ }
876
+ async migrateDownToOldest() {
877
+ await this.initialize();
878
+ const executed = await this.getExecutedMigrations();
879
+ const sorted = [...this.migrations].sort((a, b) => a.timestamp - b.timestamp);
880
+ if (executed.length === 0) {
881
+ console.log(chalk.yellow("⚠") + " No migrations to rollback");
882
+ return;
883
+ }
884
+ console.log(chalk.magenta("←") + ` Rolling back ${executed.length} migration(s)...\n`);
885
+ for (const migrationName of executed.reverse()) {
886
+ const migration = sorted.find(migration => migration.name === migrationName);
887
+ if (!migration) {
888
+ throw new Error(`Migration not found: ${migrationName}`);
889
+ }
890
+ console.log(chalk.magenta(" ←") + ` ${chalk.bold(migration.name)}`);
891
+ try {
892
+ await migration.down(this.mango);
893
+ await this.deleteOneMigrationFromDB(migration);
894
+ console.log(chalk.green(" βœ“") + ` Completed\n`);
895
+ }
896
+ catch (error) {
897
+ console.error(chalk.red(" βœ—") + ` Failed`);
898
+ console.error(chalk.dim(` Error: ${error.message}`));
899
+ throw error;
900
+ }
901
+ }
902
+ console.log(chalk.green("βœ“") + ` All migrations rolled back successfully`);
903
+ return;
904
+ }
905
+ async status() {
906
+ await this.initialize();
907
+ const executedMigrations = await this.getExecutedMigrations();
908
+ const sortedMigrations = [...this.migrations].sort((a, b) => a.timestamp - b.timestamp);
909
+ console.log(chalk.bold.cyan("\n=== Migration Status ==="));
910
+ console.log();
911
+ if (sortedMigrations.length === 0) {
912
+ console.log(chalk.dim(" No migrations registered"));
913
+ }
914
+ else {
915
+ for (const migration of sortedMigrations) {
916
+ if (executedMigrations.includes(migration.name)) {
917
+ console.log(chalk.green(" βœ“ Executed:") + ` ${chalk.cyan(migration.name)}`);
918
+ }
919
+ else {
920
+ console.log(chalk.yellow(" β§— Pending: ") + ` ${chalk.cyan(migration.name)}`);
921
+ }
922
+ }
923
+ }
924
+ console.log();
925
+ console.log(chalk.bold("Total:") + ` ${this.migrations.length} | ` +
926
+ chalk.green("Executed:") + ` ${executedMigrations.length} | ` +
927
+ chalk.yellow("Pending:") + ` ${this.migrations.length - executedMigrations.length}`);
928
+ console.log();
929
+ }
930
+ }
931
+ ;
722
932
  export { Mango, MangoType, MangoTable };
933
+ export { MangoMigration };
@@ -0,0 +1,2 @@
1
+ declare const generateMangoMigrationFile: (migrationFilename: string, outputDir?: string) => void;
2
+ export { generateMangoMigrationFile };
@@ -0,0 +1,40 @@
1
+ import chalk from "chalk";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ const generateMangoMigrationFile = (migrationFilename, outputDir = './migrations') => {
5
+ const timestamp = Date.now();
6
+ const filepath = path.join(outputDir, `${timestamp}_${migrationFilename}.ts`);
7
+ const template = `import { IMangoMigrationType, Mango } from "../src/mango.js";
8
+
9
+ export const ${migrationFilename}: IMangoMigrationType = {
10
+ name: "${migrationFilename}",
11
+ timestamp: ${timestamp},
12
+
13
+ up: async (mango: Mango) => {
14
+ // TODO: Write your migration code here
15
+ // Example:
16
+ // await mango.createTable("users", {
17
+ // id: mango.types().int().primaryKey().autoIncrement(),
18
+ // username: mango.types().varchar(255).notNull().unique(),
19
+ // password: mango.types().varchar(255).notNull()
20
+ // });
21
+
22
+ console.log("βœ“ Migration ${migrationFilename} completed");
23
+ },
24
+
25
+ down: async (mango: Mango) => {
26
+ // TODO: Write your rollback code here
27
+ // Example:
28
+ // await mango.dropTable("users");
29
+
30
+ console.log("βœ“ Rollback ${migrationFilename} completed");
31
+ }
32
+ };
33
+ `;
34
+ if (!fs.existsSync(outputDir)) {
35
+ fs.mkdirSync(outputDir, { recursive: true });
36
+ }
37
+ fs.writeFileSync(filepath, template);
38
+ console.log(chalk.green("βœ“") + ` Created migration: ${chalk.cyan(filepath)}`);
39
+ };
40
+ export { generateMangoMigrationFile };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "mango-orm",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
- "description": "A lightweight, type-safe MySQL ORM for Node.js and TypeScript",
5
+ "description": "A lightweight, type-safe MySQL ORM for Node.js and TypeScript with database migrations",
6
6
  "main": "dist/src/mango.js",
7
7
  "types": "dist/src/mango.d.ts",
8
8
  "scripts": {
@@ -10,11 +10,14 @@
10
10
  "dev": "tsc --watch",
11
11
  "test": "tsc && node dist/test/test.js",
12
12
  "test:ops": "tsc && node dist/test/test-operations.js",
13
- "prepublishOnly": "pnpm build"
13
+ "prepublishOnly": "pnpm build",
14
+ "migration:generate": "tsc && node dist/src/cli.js generate",
15
+ "migration:help": "tsc && node dist/src/cli.js help"
14
16
  },
15
17
  "files": [
16
18
  "dist/src",
17
- "README.md"
19
+ "README.md",
20
+ "LICENSE"
18
21
  ],
19
22
  "keywords": [
20
23
  "mysql",
@@ -23,7 +26,12 @@
23
26
  "database",
24
27
  "sql",
25
28
  "type-safe",
26
- "query-builder"
29
+ "query-builder",
30
+ "migrations",
31
+ "schema-management",
32
+ "mysql-orm",
33
+ "nodejs",
34
+ "fluent-api"
27
35
  ],
28
36
  "author": "Siddharth Karn siddharth2062.karn@gmail.com (https://github.com/devSiddharthKarn)",
29
37
  "license": "ISC",
@@ -38,6 +46,7 @@
38
46
  "packageManager": "pnpm@10.25.0",
39
47
  "dependencies": {
40
48
  "@types/mysql": "^2.15.27",
49
+ "chalk": "^5.6.2",
41
50
  "mysql": "^2.18.1"
42
51
  },
43
52
  "devDependencies": {