Search code examples
typescriptmikro-ormbunelysiajs

Cannot run Mikro-ORM migrations on bun


I'm currently running an all-typescript project with Bun + Elysia. I've migrated my app from a previous NestJs project, which was running MikroORM just fine. Now that I've had my app running via bun, the migrator doesn't work:

bunx --bun mikro-orm

with the following error:

354 |         /* istanbul ignore next */
355 |         if ('type' in this.options) {
356 |             throw new Error('The `type` option has been removed in v6, please fill in the `driver` option instead or use `defineConfig` helper (to define your ORM config) or `MikroORM` class (to call the `init` method) exported from the driver package (e.g. `import { defineConfig } from \'@mikro-orm/mysql\'; export default defineConfig({ ... })`).');
357 |         }
358 |         if (!this.options.driver) {
359 |             throw new Error('No driver specified, please fill in the `driver` option or use `defineConfig` helper (to define your ORM config) or `MikroORM` class (to call the `init` method) exported from the driver package (e.g. `import { defineConfig } from \'@mikro-orm/mysql\'; export defineConfig({ ... })`).');
                        ^
error: No driver specified, please fill in the `driver` option or use `defineConfig` helper (to define your ORM config) or `MikroORM` class (to call the `init` method) exported from the driver package (e.g. `import { defineConfig } from '@mikro-orm/mysql'; export defineConfig({ ... })`).
      at validateOptions (C:\Users\X\Projects\smm_est\server\node_modules\@mikro-orm\cli\node_modules\@mikro-orm\core\utils\Configuration.js:359:19)
      at new Configuration (C:\Users\X\Projects\smm_est\server\node_modules\@mikro-orm\cli\node_modules\@mikro-orm\core\utils\Configuration.js:140:13)
      at C:\Users\X\Projects\smm_est\server\node_modules\@mikro-orm\cli\node_modules\@mikro-orm\core\utils\ConfigurationLoader.js:38:24  

error: script "orm" exited with code 1

But I've specified everything in src/orm.ts file properly like this:

// orm.ts
import { Migrator } from '@mikro-orm/migrations'
import { SeedManager } from '@mikro-orm/seeder'

import { MikroORM, ReflectMetadataProvider, SqliteDriver, defineConfig } from '@mikro-orm/sqlite'
import { User } from '#user/user.entity'
import { Role } from '#role/role.entity'
import { Ability } from '#role/ability.entity'
import { Link } from '#link/link.entity'

export const config = defineConfig({
    debug: true,
    dbName: 'smm.db',
    driver: SqliteDriver,

    entities: [
        Role,
        Ability,
        User,
        Link
    ],

    extensions: [Migrator, SeedManager],
    migrations: {
        path: './src/data/migrations',
        transactional: true,
        emit: 'ts',
        snapshot: true
    },
    seeder: {
        path: './src/**/',
        glob: '!(*.d)(*.seeder).{js,ts}',
        emit: 'ts',
        defaultSeeder: 'DatabaseSeeder'
    },
    metadataProvider: ReflectMetadataProvider
})

export const orm = MikroORM.initSync(config)

export default orm

I'm running the orm with this command:

"orm": "bunx --bun mikro-orm"

This is the package.json file. Note that I do not wanna compile and produce js files:

"name": "smm_server",
  "version": "0.0.1",
  "module": "src/index.ts",
  "type": "module",
  "scripts": {
    "dev": "bun run --watch src/index.ts",
    "inspect": "bun --inspect src/index.ts",
    "format": "biome format .",
    "lint": "biome lint .",
    "check": "biome check --apply .",
    "test": "bun test",
    "typecheck": "tsc --noEmit --project tsconfig.json",
    "orm": "bunx --bun mikro-orm"
  },
  "dependencies": {
    "@elysiajs/cors": "1.0.2",
    "@elysiajs/eden": "1.0.14",
    "@elysiajs/jwt": "^1.0.2",
    "@elysiajs/static": "1.0.3",
    "@elysiajs/swagger": "1.0.5",
    "@faker-js/faker": "^8.4.1",
    "@mikro-orm/core": "^6.2.9",
    "@mikro-orm/migrations": "^6.2.9",
    "@mikro-orm/mysql": "^6.2.9",
    "@mikro-orm/reflection": "^6.2.9",
    "@mikro-orm/seeder": "^6.2.9",
    "@mikro-orm/sqlite": "^6.2.9",
    "@types/uuid": "^9.0.8",
    "elysia": "1.0.22",
    "elysia-autoroutes": "0.5.0",
    "pino": "9.1.0",
    "reflect-metadata": "^0.2.2",
    "sqlite3": "^5.1.7",
    "uuid": "^9.0.1"
  },
  "devDependencies": {
    "@biomejs/biome": "1.7.3",
    "@mikro-orm/cli": "^6.2.9",
    "bun-types": "1.1.10",
    "tslib": "2.6.2",
    "typescript": "5.4.5"
  },
  "mikro-orm": {
    "alwaysAllowTs": true,
    "configPaths": [
      "./src/data/orm.ts"
    ]
  },
  "trustedDependencies": [
    "sqlite3"
  ]
}

Also the tsconfig.json file:

{
  "compilerOptions": {
    // Enable latest features
    "lib": ["ES2022"],
    "target": "ES2022",
    "module": "Node16",
    "moduleDetection": "force",
    "jsx": "react-jsx",
    "allowJs": true,

    // Bundler mode
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "verbatimModuleSyntax": true,
    "noEmit": true,

    // Best practices
    "strict": true,
    "skipLibCheck": true,
    "noFallthroughCasesInSwitch": true,
    "types": ["bun-types"],

    // Some stricter flags
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noPropertyAccessFromIndexSignature": true,
    "forceConsistentCasingInFileNames": true,
    
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,

    "baseUrl": ".",
    "paths": {
      "#auth/*": ["./src/auth/*"],
      "#data/*": ["./src/data/*"],
      "#health/*": ["./src/health/*"],
      "#link/*": ["./src/link/*"],
      "#page/*": ["./src/page/*"],
      "#role/*": ["./src/role/*"],
      "#user/*": ["./src/user/*"],
      "#logger": ["./src/logger.ts"]
    }
  },
  "include": ["**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"],
}

I'm also adding two of these entities here:

import { Link } from '#link/link.entity'
import { Role, RoleStandard } from '#role/role.entity'

import { BeforeCreate, BeforeUpdate, Collection, Entity, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'

import { v4 } from 'uuid'

const SALT_ROUNDS = 10

@Entity({
    tableName: 'users'
})
export class User {
    @PrimaryKey({
        name: 'uuid',
        type: 'text',
        autoincrement: false,
        nullable: false,
        unique: true,
        comment: 'User\'s ID',
    })
    public uuid?: string = v4()

    @Property({
        name: 'username',
        type: 'varchar',
        length: 32,
        unique: true,
        nullable: false,
        comment: 'User\'s username'
    })
    public username?: string

    @Property({
        name: 'email',
        type: 'varchar',
        length: 64,
        unique: true,
        nullable: false,
        comment: 'User\'s email'
    })
    public email?: string

    @Property({
        name: 'display_name',
        type: 'varchar',
        length: 32,
        nullable: true,
        comment: 'User\'s display name'
    })
    public display_name?: string | null

    @Property({
        name: 'password',
        type: 'varchar',
        length: 64,
        nullable: false,
        comment: 'User\'s password'
    })
    public password?: string

    @Property({
        name: 'coins',
        type: 'integer',
        unsigned: true,
        nullable: false,
        default: 0,
        comment: 'User\'s coins'
    })
    public coins?: number

    @Property({
        name: 'created_at',
        type: 'datetime',
        nullable: false,
        comment: 'User\'s creation date and time'
    })
    public createdAt?: Date = new Date()

    /**
     * Updated at
     */
    @Property({
        name: 'updated_at',
        type: 'datetime',
        nullable: false,
        onUpdate: () => new Date(),
        comment: 'User\'s update date and time'
    })
    public updatedAt?: Date = new Date()

    @ManyToOne({
        entity: () => Role,
        nullable: false,
        deleteRule: 'set null',
    })
    public role?: Role

    @OneToMany({
        entity: () => Link,
        mappedBy: link => link.user,
        orphanRemoval: false
    })
    public links? = new Collection<Link>(this)

    @BeforeCreate()
    @BeforeUpdate()
    async hashPassword() {
        if (this.password) {
            this.password = await Bun.password.hash(this.password, {
                algorithm: 'bcrypt',
                cost: SALT_ROUNDS
            })
        }
    }

    @BeforeCreate()
    async setDefaultRole() {
        if (!this.role) {
            this.role = RoleStandard
        }
    }
}

And role.entity.ts:

import { User } from '#user/user.entity'

import { Collection, Entity, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'
import { Ability } from '#role/ability.entity'

@Entity({
    tableName: 'roles'
})
export class Role {
    constructor(id?: number, name?: string) {
        this.id = id
        this.name = name
    }

    @PrimaryKey({
        name: 'id',
        type: 'integer',
        autoincrement: false,
        comment: 'Role\'s ID',
    })
    public id?: number

    @Property({
        name: 'name',
        type: 'varchar',
        length: 16,
        unique: true,
        nullable: false,
        comment: 'Role\'s name'
    })
    public name?: string

    @OneToMany({
        entity: () => Ability,
        mappedBy: permission => permission.role,
        orphanRemoval: false
    })
    public abilities? = new Collection<Ability>(this)

    @OneToMany({
        entity: () => User,
        mappedBy: user => user.role,
        orphanRemoval: false
    })
    public users? = new Collection<User>(this)
}

export const RoleAdmin: Role = { id: 1, name: 'Admin' }
export const RolePremium: Role = { id: 2, name: 'Premium' }
export const RoleStandard: Role = { id: 3, name: 'Standard' }
export const RoleBanned: Role = { id: 4, name: 'Banned' }

Seriously I'm out of clues. When I use the orm command without MikroORM.init() (so just defineConfig()) it complains about all entities being abstract. So in the file orm.ts instead of exporting orm if I export config, it will output:

172 |     static noEntityDiscovered() {
173 |         return new MetadataError('No entities were discovered');
174 |     }
175 |     static onlyAbstractEntitiesDiscovered() {
176 |         return new MetadataError('Only abstract entities were discovered, maybe you forgot to use @Entity() decorator?');
177 |     }
                ^
MetadataError: Only abstract entities were discovered, maybe you forgot to use @Entity() decorator?
      at onlyAbstractEntitiesDiscovered (C:\Users\X\Projects\smm_est\server\node_modules\@mikro-orm\cli\node_modules\@mikro-orm\core\errors.js:177:11)
      at validateDiscovered (C:\Users\X\Projects\smm_est\server\node_modules\@mikro-orm\cli\node_modules\@mikro-orm\core\metadata\MetadataValidator.js:117:66)
      at findEntities (C:\Users\X\Projects\smm_est\server\node_modules\@mikro-orm\cli\node_modules\@mikro-orm\core\metadata\MetadataDiscovery.js:193:17)
      at C:\Users\X\Projects\smm_est\server\node_modules\@mikro-orm\cli\node_modules\@mikro-orm\core\metadata\MetadataDiscovery.js:63:14 
      at discover (C:\Users\X\Projects\smm_est\server\node_modules\@mikro-orm\cli\node_modules\@mikro-orm\core\metadata\MetadataDiscovery.js:59:25)
      at C:\Users\X\Projects\smm_est\server\node_modules\@mikro-orm\cli\node_modules\@mikro-orm\core\MikroORM.js:172:33
      at discoverEntities (C:\Users\X\Projects\smm_est\server\node_modules\@mikro-orm\cli\node_modules\@mikro-orm\core\MikroORM.js:172:33)
      at C:\Users\X\Projects\smm_est\server\node_modules\@mikro-orm\cli\node_modules\@mikro-orm\core\MikroORM.js:80:13
      at processTicksAndRejections (native:1:1)

error: script "orm" exited with code 1

This thing has been getting on my nerve lately. I thought about moving to Drizzle, but with Mikro I have the ability to switch to MySql later easily.


Solution

  • I think your problem is not using default export for the ORM config. Don't mix your config with other stuff, especially with code with side effects like yours, that is a bad idea. Put the config separately, preferably to mikro-orm.config.ts file in the root (and don't forget to use default export), then it should just work.