Logo

dev-resources.site

for different kinds of informations.

Handling TypeORM migrations in Electron apps

Published at
11/24/2024
Categories
electron
typeorm
migrations
webpack
Author
anyo
Author
4 person written this
anyo
open
Handling TypeORM migrations in Electron apps

Introduction

I'm currently building an app using Electron and decided to use SQLite with TypeORM for database management. Setting up TypeORM with Electron was relatively straightforward, but things got complicated when I started dealing with migrations.

Unlike in traditional web development, where I find migration handling fairly simple and well-documented, there isn't much guidance available for Electron apps. After spending time figuring it out, I decided to write this tutorial to share what I've learned.

Dependencies

I started with a basic Electron + TypeScript + React project and use pnpm as my package manager. This tutorial assumes you already have a working Electron app.

To set up the required libraries, install TypeORM, better-sqlite3, and their types:

pnpm add typeorm reflect-metadata better-sqlite3
pnpm add -D @types/node @types/better-sqlite3
Enter fullscreen mode Exit fullscreen mode

Additionally, import reflect-metadata somewhere in the global place of your app:

import "reflect-metadata"
Enter fullscreen mode Exit fullscreen mode

Note
I won't be covering how to create entities or migrations in this tutorial. If you're new to these concepts or need more details, I recommend checking out the TypeORM documentation on entities and migrations.

File structure

Let's start with the structure of my project:

app-name
β”œβ”€β”€ src
β”‚Β Β  β”œβ”€β”€ main
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ database
β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ dataSource.ts
β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ entities
β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ user
β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ user.entity.ts
β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  └── user.repository.ts
β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  └── post
β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β   Β Β  β”œβ”€β”€ post.entity.ts
β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β   Β Β  └── post.repository.ts
β”‚Β Β  β”‚Β Β  β”‚Β Β  └── migrations
β”‚Β Β  β”‚Β Β  β”‚Β Β      β”œβ”€β”€ 1738490591309-createUsersTable.ts
β”‚Β Β  β”‚Β Β  β”‚Β Β      └── 1738490598615-createPostsTable.ts
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ ipc
β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ users.ts # createUser(), getUsers(), updateUser()...
β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ posts.ts # createPost(), getPosts(), updatePost()...
β”‚Β Β  β”‚Β Β  β”‚Β Β  └── index.ts
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ utils
β”‚Β Β  β”‚Β Β  β”‚Β Β  └── ...
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ index.ts
β”‚Β Β  β”‚Β Β  └── windowManager.ts
β”‚Β Β  β”œβ”€β”€ preload
β”‚Β Β  β”‚Β Β  └── ...
β”‚Β Β  β”œβ”€β”€ renderer
β”‚Β Β  β”‚Β Β  └── ... (React app)
β”‚Β Β  └── shared
β”‚Β Β      └── ...
β”œβ”€β”€ forge.config.ts
β”œβ”€β”€ package.json
β”œβ”€β”€ pnpm-lock.yaml
β”œβ”€β”€ tsconfig.json
β”œβ”€β”€ webpack.main.config.ts
β”œβ”€β”€ webpack.plugins.ts
β”œβ”€β”€ webpack.renderer.config.ts
β”œβ”€β”€ webpack.rules.ts
└── ...
Enter fullscreen mode Exit fullscreen mode

Configuration details

package.json

Add the following scripts to your package.json.
The migrate:* scripts are optional but useful for common tasks.

TypeORM documentation: Using CLI > If entities files are in typescript

"scripts": {
    // ...
    "typeorm": "typeorm-ts-node-commonjs",
    "rebuild": "electron-rebuild -f -w better-sqlite3",
    "postinstall": "electron-rebuild -f -w better-sqlite3",
    "migrate:create": "sh -c 'pnpm typeorm migration:create ./src/main/database/migrations/$1' --",
    "migrate:up": "pnpm typeorm -d ./src/main/database/dataSource.ts migration:run",
    "migrate:down": "pnpm typeorm -d ./src/main/database/dataSource.ts migration:revert"
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Usage:

pnpm typeorm -h
pnpm rebuild
pnpm migrate:create [migrationName] # e.g., pnpm migrate:create createUsersTable
pnpm migrate:up
pnpm migrate:down
Enter fullscreen mode Exit fullscreen mode

webpack.main.config.ts

The migrations bundling is done with webpack.
TypeORM has documentation on bundling migration files: Bundling Migration Files.

If glob is not installed, add it as a dev dependency:

pnpm add -D glob
Enter fullscreen mode Exit fullscreen mode

Here is my full webpack config for the main process:

// webpack.main.config.ts
import * as glob from "glob";
import path from "path";
import { Configuration } from "webpack";

import { plugins } from "./webpack.plugins";
import { rules } from "./webpack.rules";

const indexEntryName = "index";
export const mainConfig: Configuration = {
  resolve: {
    extensions: [".js", ".ts", ".jsx", ".tsx", ".css", ".json"],
  },
  entry: {
    [indexEntryName]: "./src/main/index.ts",
    ...glob
      .sync(path.resolve("src/main/database/migrations/*.ts"))
      .reduce((entries: Record<string, string>, filename: string) => {
        const migrationName = path.basename(filename, ".ts");
        return Object.assign({}, entries, { [migrationName]: filename });
      }, {}),
  },
  output: {
    libraryTarget: "umd",
    filename: (pathData) => {
      return pathData.chunk?.name && pathData.chunk.name !== indexEntryName
        ? "database/migrations/[name].js"
        : "[name].js";
    },
  },
  plugins,
  module: {
    rules,
  },
  optimization: {
    minimize: false,
  },
};
Enter fullscreen mode Exit fullscreen mode

src/main/database/dataSource.ts

Create a DataSource file for configuring database connection settings:

// src/main/database/dataSource.ts
import path from "node:path";
import "reflect-metadata";
import { DataSource } from "typeorm";

import { UserEntity } from "./entities/user/user.entity";
import { PostEntity } from "./entities/post/post.entity";

const isElectron = !!process.versions.electron; // simple trick to see if the data source is called from the Electron app or CLI (for migrations scripts)
const isProduction = process.env.NODE_ENV === "production";

let databasePath: string;

if (isElectron) {
  // eslint-disable-next-line @typescript-eslint/no-require-imports
  const { app } = require("electron");

  databasePath = path.join(
    app.getPath("userData"),
    app.isPackaged ? "app-name.sqlite" : "app-name.dev.sqlite"
  );
} else {
  // use hardcoded path for running migrations in development (macOS)
  databasePath = path.join(
    "/Users/user/Library/Application Support/app-name/",
    isProduction ? "app-name.sqlite" : "app-name.dev.sqlite"
  );
}

// https://typeorm.io/data-source-options#better-sqlite3-data-source-options
const dataSource = new DataSource({
  type: "better-sqlite3",
  database: databasePath,
  entities: [UserEntity, PostEntity],
  migrations: [
    path.join(__dirname, isElectron ? "database" : "", "/migrations/*.{js,ts}"),
  ],
  synchronize: false, // important
  logging: true // use this for debugging
});

export const entityManager = dataSource.createEntityManager();
export default dataSource;
Enter fullscreen mode Exit fullscreen mode

src/main/index.ts

Add a setupDatabase function to initialize the database on every app launch:

// src/main/index.ts
import { app } from "electron";
import dataSource from "./database/dataSource";

// ...

const setupDatabase = async () => {
  try {
    await dataSource.initialize();
    console.info("Database initialized");
    const pendingMigrations = await dataSource.showMigrations();
    console.info("Pending migrations:", pendingMigrations);
    if (pendingMigrations) {
      console.info("Running migrations...");
      await dataSource.runMigrations();
      console.info("Migrations completed");
    }
  } catch (err) {
    console.error(err);
  }
};

app.whenReady().then(async () => {
    // ...
    await setupDatabase(); // do this before createWindow()
    // ...
});

// ...
Enter fullscreen mode Exit fullscreen mode

Conclusion

I hope dealing with migrations in Electron apps will now be straightforward for you. This setup took me a while to figure out, so I'm glad to share it and hopefully save you some time. Thank you for reading!

electron Article's
30 articles in total
Favicon
First thoughts on Electron
Favicon
Let's build website builder
Favicon
Study With Me 1.0
Favicon
[Boost]
Favicon
Electric Bus Pantograph Market: Trends, Challenges, Drivers, and Insights Through 2033
Favicon
Keyboard Sounds β€” Make any keyboard sound mechanical
Favicon
Electron
Favicon
I Hate Website Builders – So I Built My Own
Favicon
Is the browser always the right tool for the job?
Favicon
E-House Market Insights: Compact Solutions for Modern Power Challenges
Favicon
.NET Cross-Platform Web Desktop App Frameworks as Electron Alternatives
Favicon
How to remotely EV code-sign a windows application using ssl.com
Favicon
Configuring webpack to handle multiple browser windows in Electron
Favicon
Using native modules in Electron
Favicon
Requesting camera and microphone permission in an Electron app
Favicon
πŸš€Building a Multi-Step Loading Screen with Electron
Favicon
Building deep-links in Electron application
Favicon
MaweJS: Editor for plantsers
Favicon
Handling TypeORM migrations in Electron apps
Favicon
Unicode-Search - my first Electron app!
Favicon
Creating a synchronized store between main and renderer process in Electron
Favicon
The ultimate Electron app with Next.js and React Server Components
Favicon
Electric Bikes And Coding
Favicon
Creating a Browser Window in Electron: A Step-by-Step Guide
Favicon
How to Create a Windows Executable with Electron Forge that Adds a Desktop Shortcut?
Favicon
Building and publishing an Electron application using electron-builder
Favicon
Cross-compile a distributed Electron App
Favicon
The Only Electron Framework You'll Ever Need: Introducing the Ideal Electron Framework
Favicon
Overcoming Electron-Builder Limitations: A C# and NSIS Hybrid Approach
Favicon
How to Use Electron.js to Create Cross-Platform Desktop Applications

Featured ones: