Typeorm Extension π
This is a library to
create
, drop
& seed
the (default-) database π₯- manage one or many data-source instances π»
- parse & apply query parameters (extended JSON:API specification & fully typed) to:
filter
(related) resources according to one or more criteria,- reduce (related) resource
fields
, include
related resources,sort
resources according to one or more criteria,- limit the number of resources returned in a response by
page
limit & offset
Warning
This readme includes the documentation for the upcoming version 3.
This is the link for the v2.
Table of Contents
Installation
npm install typeorm-extension --save
Documentation
To read the docs, visit https://typeorm-extension.tada5hi.net
Usage
CLI
If you use esm, the executable must be changed from typeorm-extension
to typeorm-extension-esm
.
The following commands are available in the terminal:
typeorm-extension db:create
to create the databasetypeorm-extension db:drop
to drop the databasetypeorm-extension seed:run
seed the databasetypeorm-extension seed:create
to create a new seeder
If the application has not yet been built or is to be tested with ts-node, the commands can be adapted as follows:
"scripts": {
"db:create": "ts-node ./node_modules/typeorm-extension/bin/cli.cjs db:create",
"db:drop": "ts-node ./node_modules/typeorm-extension/bin/cli.cjs db:drop",
"seed:run": "ts-node ./node_modules/typeorm-extension/bin/cli.cjs seed:run",
"seed:create": "ts-node ./node_modules/typeorm-extension/bin/cli.cjs seed:create"
}
To test the application in the context of an esm project, the following adjustments must be made:
- executable
ts-node
to ts-node-esm
- library path
cli.cjs
to cli.mjs
Read the Seeding Configuration section to find out how to specify the path,
for the seeder- & factory-location.
CLI Options
Option | Commands | Default | Description |
---|
--root or -r | db:create , db:drop , seed:create & seed:run | process.cwd() | Root directory of the project. |
--dataSource or -d | db:create , db:drop & seed:run | data-source | Name (or relative path incl. name) of the data-source file. |
--synchronize or -s | db:create & db:drop | yes | Synchronize the database schema after database creation. Options: yes or no . |
--initialDatabase | db:create | undefined | Specify the initial database to connect to. This option is only relevant for the postgres driver, which must always to connect to a database. If no database is provided, the database name will be equal to the connection user name. |
--name | seed:create & seed:run | undefined | Name (or relative path incl. name) of the seeder. |
--preserveFilePaths | db:create , db:drop , seed:create & seed:run | false | This option indicates if file paths should be preserved and treated as if the just-in-time compilation environment is detected. |
CLI Examples
Database Create
ts-node ./node_modules/typeorm-extension/bin/cli.cjs db:create -d src/data-source.ts
Database Drop
ts-node ./node_modules/typeorm-extension/bin/cli.cjs db:drop -d src/data-source.ts
Seed Run
ts-node ./node_modules/typeorm-extension/bin/cli.cjs seed:run -d src/data-source.ts
Seed Run Explicit
ts-node ./node_modules/typeorm-extension/bin/cli.cjs seed:run -d src/data-source.ts --name src/database/seeds/user.ts
Seed Create
ts-node ./node_modules/typeorm-extension/bin/cli.cjs seed:create --name src/database/seeds/user.ts
Database
An alternative to the CLI variant, is to create
the database in the code base during the runtime of the application.
Therefore, provide the DataSourceOptions
for the DataSource manually, or let it be created automatically:
Create
Example #1
import { DataSource, DataSourceOptions } from 'typeorm';
import { createDatabase } from 'typeorm-extension';
(async () => {
const options: DataSourceOptions = {
type: 'better-sqlite',
database: 'db.sqlite'
};
await createDatabase({
options
});
const dataSource = new DataSource(options);
await dataSource.initialize();
})();
Example #2
import {
buildDataSourceOptions,
createDatabase
} from 'typeorm-extension';
(async () => {
const options = await buildDataSourceOptions();
await createDatabase({
options
});
const dataSource = new DataSource(options);
await dataSource.initialize();
})();
Example #3
It is also possible to let the library automatically search for the data-source under the hood.
Therefore, it will search by default for a data-source.{ts,js}
file in the following directories:
{src,dist}/db/
{src,dist}/database
{src,dist}
import { createDatabase } from 'typeorm-extension';
(async () => {
await createDatabase();
})();
To get a better overview and understanding of the
createDatabase
function, check out the documentation.
Drop
Example #1
import {
DataSource,
DataSourceOptions
} from 'typeorm';
import { dropDatabase } from 'typeorm-extension';
(async () => {
const options: DataSourceOptions = {
type: 'better-sqlite',
database: 'db.sqlite'
};
await dropDatabase({
options
});
})();
Example #2
import {
buildDataSourceOptions,
dropDatabase
} from 'typeorm-extension';
(async () => {
const options = await buildDataSourceOptions();
await dropDatabase({
options
});
})();
Example #3
It is also possible to let the library automatically search for the data-source under the hood.
Therefore, it will search by default for a data-source.{ts,js}
file in the following directories:
{src,dist}/db/
{src,dist}/database
{src,dist}
import { dropDatabase } from 'typeorm-extension';
(async () => {
await dropDatabase();
})();
To get a better overview and understanding of the
dropDatabase
function, check out the documentation.
Instances
Single
The default DataSource instance can be acquired, by not providing any alias at all or using the key default
.
If no DataSource instance or DataSourceOptions object is deposited initially the method will attempt to locate and load
the DataSource file and initialize itself from there.
import { useDataSource } from 'typeorm-extension';
(async () => {
const dataSource : DataSource = await useDataSource();
})();
Reference(s):
Multiple
It is also possible to manage multiple DataSource instances.
Therefore, each additional DataSource must be registered under a different alias.
This can be done by either setting the DataSource instance or the DataSourceOptions object for the given alias.
import { DataSource, DataSourceOptions } from 'typeorm';
import { setDataSource, useDataSource } from 'typeorm-extension';
(async () => {
const secondDataSourceOptions : DataSourceOptions = {
};
const dataSource = new DataSource(secondDataSourceOptions);
setDataSource(dataSource, 'second');
const instance : DataSource = await useDataSource('second');
})();
Reference(s):
Seeding
Seeding the database is fairly easy and can be achieved by following the steps below:
Configuration
: Specify the seed and factory location by path or object.Entity
: Define one or more entities.Factory
(optional): Define a factory for each entity for which data should be automatically generated.Seed
: Define one or more seed classes to populate the database with an initial data set or generated data by a factory.Execute
: Run the seeder(s) with the CLI or in the code base.
Configuration
Seeder paths are configured as glob patterns, making it easy
to match all the factory/seeder files in your project without configuration effort:
- use
*
to match anything expect slashes and hidden files - use
**
to match zero or more directories - use comma separate values between
{}
to match against a list of options
Check out the glob documentation for other supported pattern features.
It is important to use the posix/unix path separator (/) because
the Windows path separator (\) is used to match paths with literal global pattern characters.
The seeder- & factory-location, can be specified via:
environment
variable(s)- extended
data-source.ts
file runSeeder(s)
method options parameter, in case of a direct code base usage
The following values are assumed by default:
- factory path:
src/database/factories/**/*{.ts,.js}
- seed path:
src/database/seeds/**/*{.ts,.js}
Note: When seeder paths are configured as glob patterns, the paths are resolved and sorted in alphabetical order using filenames. This helps to ensure that the seeders are executed in the correct order.
It is possible to define that a seeder is only executed once.
This can either be set globally using the seedTacking option or locally using the track property of a seeder class.
data-source.ts
import { DataSource, DataSourceOptions } from 'typeorm';
import { SeederOptions } from 'typeorm-extension';
const options: DataSourceOptions & SeederOptions = {
type: 'better-sqlite',
database: 'db.sqlite',
seeds: ['src/database/seeds/**/*{.ts,.js}'],
seedTracking: false,
factories: ['src/database/factories/**/*{.ts,.js}'],
};
export const dataSource = new DataSource(options);
runSeeder(s)
import { DataSource, DataSourceOptions } from 'typeorm';
import { runSeeders, SeederOptions } from 'typeorm-extension';
(async () => {
const options: DataSourceOptions = {
type: 'better-sqlite',
database: 'db.sqlite',
};
const dataSource = new DataSource(options);
await dataSource.initialize();
runSeeders(dataSource, {
seeds: ['src/database/seeds/**/*{.ts,.js}'],
factories: ['src/database/factories/**/*{.ts,.js}']
});
})();
Entity
To get started, define one or more entities.
user.ts
import {
Entity,
PrimaryGeneratedColumn,
Column
} from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number
@Column()
firstName: string
@Column()
lastName: string
@Column()
email: string
}
Factory
To create entities with random data, create a factory for each desired entity.
The definition of a factory is optional.
The factory callback provides an instance of the faker library as function argument,
to populate the entity with random data.
user.factory.ts
import { setSeederFactory } from 'typeorm-extension';
import { User } from './user';
export default setSeederFactory(User, (faker) => {
const user = new User();
user.firstName = faker.name.firstName('male');
user.lastName = faker.name.lastName('male');
user.email = faker.internet.email(user.firstName, user.lastName);
return user;
})
Seed
And last but not least, create a seeder. The seeder can be called by the cli command seed
or in the codebase
by using the function runSeeder
.
A seeder class only requires one method, called run
and provides the arguments dataSource
& factoryManager
.
user.seeder.ts
A seeder class must implement the Seeder interface, and could look like this:
import { Seeder, SeederFactoryManager } from 'typeorm-extension';
import { DataSource } from 'typeorm';
import { User } from './user';
export default class UserSeeder implements Seeder {
track = false;
public async run(
dataSource: DataSource,
factoryManager: SeederFactoryManager
): Promise<any> {
const repository = dataSource.getRepository(User);
await repository.insert([
{
firstName: 'Caleb',
lastName: 'Barrows',
email: 'caleb.barrows@gmail.com'
}
]);
const userFactory = await factoryManager.get(User);
await userFactory.save();
await userFactory.saveMany(5);
}
}
Execute
Populate the database from the code base:
import { DataSource, DataSourceOptions } from 'typeorm';
import { runSeeders, SeederOptions } from 'typeorm-extension';
import { User } from 'user';
(async () => {
const options: DataSourceOptions & SeederOptions = {
type: 'better-sqlite',
database: 'db.sqlite',
entities: [User],
seeds: ['./*.seeder.ts'],
factories: ['./*.factory.ts']
};
const dataSource = new DataSource(options);
await dataSource.initialize();
await runSeeders(dataSource);
})();
Populate the database by explicit definitions from the codebase.
import { DataSource, DataSourceOptions } from 'typeorm';
import { runSeeders, SeederOptions } from 'typeorm-extension';
import { User } from 'user';
import UserSeeder from 'user.seeder';
import UserFactory from 'user.factory';
(async () => {
const options: DataSourceOptions & SeederOptions = {
type: 'better-sqlite',
database: 'db.sqlite',
entities: [User],
seeds: [UserSeeder],
factories: [UserFactory]
};
const dataSource = new DataSource(options);
await dataSource.initialize();
await runSeeders(dataSource);
})();
Query
The query submodule enables query parameter (fields, filter, ...) values to be build, parsed & validated.
Therefore, the rapiq library is used under the hood.
The query parameter options (allowed, default, ...) are fully typed π₯ and depend on the (nested-) properties of the target entity passed to
the typeorm query builder.
For explanation proposes,
two simple entities with a relation between them are declared to demonstrate the usage of the query utils:
import {
Entity,
PrimaryGeneratedColumn,
Column,
OneToOne,
JoinColumn
} from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn({unsigned: true})
id: number;
@Column({type: 'varchar', length: 30})
@Index({unique: true})
name: string;
@Column({type: 'varchar', length: 255, default: null, nullable: true})
email: string;
@OneToOne(() => Profile)
profile: Profile;
}
@Entity()
export class Profile {
@PrimaryGeneratedColumn({unsigned: true})
id: number;
@Column({type: 'varchar', length: 255, default: null, nullable: true})
avatar: string;
@Column({type: 'varchar', length: 255, default: null, nullable: true})
cover: string;
@OneToOne(() => User)
@JoinColumn()
user: User;
}
In this example routup and the
plugin @routup/query is used to handle HTTP requests,
but there is also a guide available for express.
import { createServer } from 'node:http';
import type { Request, Response } from 'routup';
import { createNodeDispatcher, Router } from 'routup';
import { createHandler, useQuery } from '@routup/query';
import {
applyQuery,
useDataSource
} from 'typeorm-extension';
const router = new Router();
router.use(createHandler());
router.get('users', async (req: Request, res: Response) => {
const dataSource = await useDataSource();
const repository = dataSource.getRepository(User);
const query = repository.createQueryBuilder('user');
const { pagination } = applyQuery(query, useQuery(req), {
defaultAlias: 'user',
fields: {
allowed: ['id', 'name', 'profile.id', 'profile.avatar'],
},
filters: {
allowed: ['id', 'name', 'profile.id'],
},
pagination: {
maxLimit: 20
},
relations: {
allowed: ['profile']
},
sort: {
allowed: ['id', 'name', 'profile.id']
},
});
const [entities, total] = await query.getManyAndCount();
return {
data: entities,
meta: {
total,
...pagination
}
};
});
const server = createServer(createNodeDispatcher(router));
server.listen(80);
Contributing
Before starting to work on a pull request, it is important to review the guidelines for
contributing and the code of conduct.
These guidelines will help to ensure that contributions are made effectively and are accepted.
License
Made with π
Published under MIT License.