Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migration guide from v0.2.x to v0.3.x #9286

Open
needim opened this issue Aug 14, 2022 · 6 comments
Open

Migration guide from v0.2.x to v0.3.x #9286

needim opened this issue Aug 14, 2022 · 6 comments

Comments

@needim
Copy link
Contributor

needim commented Aug 14, 2022

I know this is not a feature request but maybe this issue can help other people to upgrade. Because I see a lot of people struggling with version update to v0.3.x from v0.2.x.

I finally managed to update our codebase for v0.3.x and wanted to share some paths I followed.

  1. I had to move all custom repositories to independent services which are using plain repositories. No use for custom repositories anymore. (I don't like it but ok)

  2. I had to create my own DataSourceManager (for replacement of ConnectionManager in v0.2.x) because we are using multitenant DB structures, on the fly creating data sources and destroying them if necessary.

    data-source.ts

    import config from 'config';
    import 'reflect-metadata';
    import { DataSource, DataSourceOptions, EntityTarget, ObjectLiteral, Repository } from 'typeorm';
    import { ORMLogger } from './utils/ORMLogger';
    
    export const dataSourceOptions: Map<string, DataSourceOptions> = new Map([
    [
        'default',
        {
        type: 'mysql',
        host: config.get('dbHost'),
        charset: 'utf8mb4_unicode_ci',
        port: config.get('dbPort'),
        username: config.get('dbUser'),
        password: config.get('dbPass'),
        database: config.get('adminDbName'),
        logger: new ORMLogger(),
        acquireTimeout: 50000,
        connectTimeout: 50000,
        logging: process.env.NODE_ENV !== 'production',
        migrationsRun: true,
        entities: [__dirname + '/models/admin/*.{js,ts}'],
        migrations: [__dirname + '/migrations/*.{js,ts}'],
        subscribers: [__dirname + '/subscribers/**/*.{js,ts}'],
        bigNumberStrings: true,
        },
    ],
    [
        'mongo-logger',
        {
        type: 'mongodb',
        host: config.get('mongoHost'),
        port: config.get('mongoPort'),
        username: config.get('mongoUser'),
        password: config.get('mongoPass'),
        database: config.get('mongoDB'),
        authSource: 'admin',
        synchronize: true,
        logging: false,
        useUnifiedTopology: true,
        useNewUrlParser: true,
        entities: [__dirname + '/models/mongo/*.{js,ts}'],
        migrations: [__dirname + '/migrations/*.{js,ts}'],
        subscribers: [__dirname + '/subscribers/*.{js,ts}'],
        },
    ],
    ['companyContext', getCompanyDataSource(config.get('dbName'), true)],
    ]);
    
    // to be used instead of getConnectionManager()
    export class DataSourceManager {
    private static instance: DataSourceManager;
    private dataSources: Map<string, DataSource> = new Map();
    
    private constructor() {}
    
    public static getInstance(): DataSourceManager {
        if (!DataSourceManager.instance) {
        DataSourceManager.instance = new DataSourceManager();
        }
        return DataSourceManager.instance;
    }
    
    public async createDataSource(name: string, options?: DataSourceOptions): Promise<DataSource> {
        const _options = options || dataSourceOptions.get(name);
        return this.dataSources.set(name, await new DataSource(_options).initialize()).get(name);
    }
    
    public getDataSource(dataSourceName: string): DataSource {
        return this.dataSources.get(dataSourceName || 'default');
    }
    
    private map() {
        return Array.from(this.dataSources.values());
    }
    
    public getActiveDataSources() {
        return this.map().filter((ds: DataSource) => ds.isInitialized);
    }
    
    public debug() {
        console.log(
        'DataSourceManager.debug',
        this.getActiveDataSources().map((ds: DataSource) => {
            return { isInitialized: ds.isInitialized, type: ds.options.type, dbName: ds.options.database };
        })
        );
    }
    
    public async destroyDataSources() {
        return Promise.all(Array.from(this.dataSources.values()).map((dataStore) => dataStore.destroy()))
        .then((data) => {
            console.log(`Destroyed (${data.length}) data source`);
        })
        .catch((errors) => {
            console.log('Error: Destroying DataSources:', errors);
        });
    }
    }
    
    export function getCompanyDataSource(databaseName: string, migrationsRun = true): DataSourceOptions {
    return {
        type: 'mysql',
        charset: 'utf8mb4_unicode_ci',
        host: config.get('dbHost'),
        port: Number(config.get('dbPort')),
        username: config.get('dbUser').toString(),
        password: config.get('dbPass').toString(),
        logger: new ORMLogger(),
        database: databaseName,
        acquireTimeout: 50000,
        connectTimeout: 50000,
        logging: process.env.NODE_ENV !== 'production',
        entities: [__dirname + '/models/*.{js,ts}', __dirname + '/modules/**/models/*.{js,ts}'],
        migrations: [__dirname + '/migrations/companyContext/*.{js,ts}'],
        subscribers: [__dirname + '/subscribers/**/*.{js,ts}'],
        migrationsRun,
        bigNumberStrings: true,
    };
    }
    
    // HELPER FUNCTIONS FOR BETTER TYPEORM v0.2.x -> v0.3.x UPDATE
    // to be used instead of getRepository()
    export function getDataRepository<Entity extends ObjectLiteral>(
    entityClass: EntityTarget<Entity>,
    dataSourceName?: string
    ): Repository<Entity> {
    return DataSourceManager.getInstance()
        .getDataSource(dataSourceName || 'default')
        .getRepository(entityClass);
    }
    
    // to be used instead of getConnection()
    export function getDataSource(dataSourceName?: string): DataSource {
    return DataSourceManager.getInstance().getDataSource(dataSourceName || 'default');
    }
    
  3. For CLI I had to export some default dataSources from my own DataSourceManager in seperate files.

    package.json old

        "typeorm": "ts-node --transpile-only ./node_modules/typeorm/cli.js",
    

    package.json new

        "typeorm": "typeorm-ts-node-commonjs",
    

    3.1. Migration run, generate, seed commmands had to change.

    for default context from:

    npm run typeorm migration:run
    npm run typeorm migration:generate -- -n TestMigration
    npm run typeorm migration:create -- -n TestSeeds

    changed to:

    npm run typeorm migration:run -- -d src/data-source-admin.ts
    npm run typeorm migration:generate -- -d src/data-source-admin.ts src/migrations/TestMigration
    npm run typeorm migration:create -- src/migrations/TestSeeds
    

    for company context from:

    npm run typeorm migration:run -- -c companyContext
    npm run typeorm migration:generate -- -n TestMigration -c companyContext
    npm run typeorm migration:create -- -n TestSeeds

    changed to:

    npm run typeorm migration:run -- -d src/data-source-company.ts
    npm run typeorm migration:generate -- -d src/data-source-company.ts src/migrations/companyContext/TestMigration
    npm run typeorm migration:create -- src/migrations/companyContext/TestSeeds
    

    data-source-admin.ts (new file which is used for admin context migration cli)

    import { DataSource } from 'typeorm';
    import { dataSourceOptions } from './data-source';
    
    const adminDataSourceOptions = dataSourceOptions.get('default');
    
    // this file is only for cli migration utils
    export default new DataSource(adminDataSourceOptions);
    

    data-source-company.ts (new file which is used for company context migration cli)

    import { DataSource } from 'typeorm';
    import { dataSourceOptions } from './data-source';
    
    const companyDataSourceOptions = dataSourceOptions.get('companyContext');
    
    // this file is only for cli migration utils
    export default new DataSource(companyDataSourceOptions);
    
    
  4. And I had to convert old usages to new usages. Here are some regex replacers I used for in VSCode:

    find: findOne\(([\w.]+)\)
    repl: findOne({where: {id: $1}})
    
    find: findOne\(([\w.]+), \{
    repl: findOne({where: {id: $1},
    
    find: FindConditions
    repl: FindOptionsWhere
    
  5. And also I did a lot of manual changes in the codebase for new usages

@killjoy2013
Copy link

Thanks @needim , very valuable!
Wish there was a clear migration guide in the official docs :-(

@alexphamhp
Copy link

Thank you @needim for this great guide.
I have a question with using -- ~ npm run typeorm migration:run -- -d src/data-source-admin.ts
Why we have to set it ?
I tried to run without it and the result is still the same . Can anyone tell me why ?
npm run typeorm migration:run -d src/data-source-admin.ts

@troywweber7
Copy link

troywweber7 commented Mar 21, 2023

Has anyone noticed when upgrading that default join table names changed from entity1_prop__entity2 to entity1_prop_entity2 with one less underscore. What is the recommended approach to how to handle this? Will creating an automatic migration with typeorm migration:generate resolve this properly, or should we just create a migration to rename the tables manually?

UPDATE:

What I ended up doing to address this change in default join table names was the following:

  1. Create manual migration which just renames the appropriate join tables.
  2. Run manual name change migration.
  3. Generate automatic migration to see what else typeorm automatically picks up.
  4. Run automatic migration and verify behavior is working.

If everything is working, you can probably merge the manual table name migration with the auto-generated one since these need to happen at the same time. But you'll have to be sure to revert anything you ran before merging them and then re-run the merged logic. If anyone has a better solution/answer, I'll still be curious of course.

UPDATE 2:

That doesn't seem to have fully worked as I expected. It seems to still be looking for the __ join tables even though they were renamed and the auto-migration stuff was run. My next approach will be to use the name parameter in the @JoinTable() decorator to keep the legacy join table's name. Still looking for alternatives for how to upgrade properly.

@NipunaC95
Copy link

There is an issue what if we wanna give the migration name as an argument without the file path to migrations ? We can't do this according to the above implementation

@clintonb
Copy link
Contributor

I wrote some codemods to perform this migration. See https://dev.clintonblackburn.com/2023/07/15/upgrading-typeorm-with-jscodeshift for more info.

@gabrielmcreynolds
Copy link

@needim Thanks for this comprehensive guide on migrating. Has anyone had any issues with generating migrations after following these steps. When I run the CLI commands given I get the error:

TypeError: Cannot read properties of undefined (reading 'name')
at MysqlQueryRunner.changeColumn

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants