TypeORM is a great option if you are looking for an ORM to use on a Node project. However, if you use Webpack to bundle your backend code, you may face some issues when trying to integrate TypeORM into your project.
The Problem
After you’ve integrated TypeORM into your project, you should have no problem building your project. However, if you try running your bundled code, you will see something similar to the following error:
import {Entity, PrimaryGeneratedColumn, Column} from “typeorm”; ^^^^^^ SyntaxError: Cannot use import statement outside a module
This happens because TypeORM is running inside the Webpack build and trying to require the un-compiled code that is specified in the entities
, migrations
, and subscribers
entries in the ormconfig
.
Potential Solution #1
When I first encountered this issue, I tried to solve it by excluding my entities and migrations from being bundled in the Webpack output, then specifying the compiled entities in the ormconfig
:
{
...
"entities": ["dist/src/entity/**/*.js"],
"migrations": ["dist/src/migration/**/*.js"],
"subscribers": ["dist/src/subscriber/**/*.js"],
...
}
This worked when I ran the server code but caused tools like Jest that don’t compile the code to choke.
Potential Solution #2
Another solution I tried was to not rely on our ormconfig
to specify where our entities and migrations are coming from. Instead, it’s possible to import them ourselves when we call createConnection
:
import { User } from 'src/entity/User.ts';
const baseConnectionOpts = await getConnectionOptions();
await createConnection({
...baseConnectionOpts,
entities: [
User // pass your entities in here
]
})
This works for both Jest and the server build! However, we no longer get the convenience of automatically detecting any entities or migrations that are added to our project.
In order to change that, we’ll need a way to import all of the desired exports from a given directory.
require.context to the Rescue!
Webpack gives us require.context
, which allows us to get all matching modules from a given directory. This means we can get all of our entities and migrations automatically!
import {
Connection,
ConnectionOptions,
createConnection as createTypeOrmConnection,
getConnection,
getConnectionOptions,
} from "typeorm";
type EntitiesAndMigrationsOpts = Pick<
ConnectionOptions,
"entities" | "migrations"
>;
const importAllFunctions = (
requireContext: __WebpackModuleApi.RequireContext
) =>
requireContext
.keys()
.sort()
.map((filename) => {
const required = requireContext(filename);
return Object.keys(required).reduce((result, exportedKey) => {
const exported = required[exportedKey];
if (typeof exported === "function") {
return result.concat(exported);
}
return result;
}, [] as any);
})
.flat();
const entitiesViaWebpack: NonNullable<
EntitiesAndMigrationsOpts["entities"]
> = importAllFunctions(require.context("./entity/", true, /\.ts$/));
const migrationsViaWebpack: NonNullable<
EntitiesAndMigrationsOpts["migrations"]
> = importAllFunctions(require.context("./migration/", true, /\.ts$/));
export const createConnection = async (): Promise<Connection> => {
const baseConnectionOptions = await getConnectionOptions();
const connectionOptions: ConnectionOptions = {
...baseConnectionOptions,
entities: entitiesViaWebpack,
migrations: migrationsViaWebpack,
};
return createTypeOrmConnection(connectionOptions);
};
Note: If you are using Typescript, make sure to yarn add@types/webpack-env
so you can use require.context
without type errors.
But What About Jest?
While the above solution works when running our backend, it will not work when we want to create connections in Jest. This is because require.context
is only available when running in code bundled by Webpack.
We can fix this by extracting our require.context
calls to another file:
//entities-and-migrations.ts
import { ConnectionOptions } from "typeorm";
export type EntitiesAndMigrationsOpts = Pick<ConnectionOptions, "entities" | "migrations">;
const importAllFunctions = (
requireContext: __WebpackModuleApi.RequireContext
) =>
requireContext
.keys()
.sort()
.map((filename) => {
const required = requireContext(filename);
return Object.keys(required).reduce((result, exportedKey) => {
const exported = required[exportedKey];
if (typeof exported === "function") {
return result.concat(exported);
}
return result;
}, [] as any);
})
.flat();
const entitiesViaWebpack: NonNullable< EntitiesAndMigrationsOpts["entities"] > = importAllFunctions(require.context("./entity/", true, /\.ts$/));
const migrationsViaWebpack: NonNullable< EntitiesAndMigrationsOpts["migrations"] > = importAllFunctions(require.context("./migration/", true, /\.ts$/));
export const entitiesAndMigrations: EntitiesAndMigrationsOpts = {
entities: entitiesViaWebpack,
migrations: migrationsViaWebpack,
};
And then conditionally requiring that file, like so:
import {
Connection,
ConnectionOptions,
createConnection as createTypeOrmConnection,
getConnection,
getConnectionOptions,
} from "typeorm";
type CreateConnectionOpts = {
loadEntitiesAndMigrations: boolean;
};
export const createConnection = async (
opts: CreateConnectionOpts
): Promise => {
let entitiesAndMigrations = null;
if (opts.loadEntitiesAndMigrations) {
entitiesAndMigrations = require("./entities-and-migrations")
.entitiesAndMigrations;
}
const baseConnectionOptions = await getConnectionOptions();
const connectionOptions: ConnectionOptions = {
...baseConnectionOptions,
...entitiesAndMigrations,
};
return createTypeOrmConnection(connectionOptions);
};
By putting require("./entities-and-migrations")
into a conditional, we only call require.context
when loadEntitiesAndMigrations
is set to true. Otherwise, TypeORM uses the values set in our ormconfig
. This allows us to create connections in a test environment by setting loadEntitiesAndMigrations
to false!
If you would like to see this code in action, I pushed a sample project to github.
It was really helpful. Thanks you
You save me. Thank you.