├── CNAME ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── bug-report.md │ └── suggest-a-feature.md ├── actions │ ├── build │ │ └── action.yml │ └── install │ │ └── action.yml ├── dependabot.yml └── workflows │ ├── main.yml │ └── release.yml ├── writable └── .gitignore ├── src ├── helpers │ ├── index.ts │ └── entity │ │ ├── index.ts │ │ ├── metadata.ts │ │ ├── property-names.ts │ │ ├── error.ts │ │ ├── join-columns.ts │ │ └── uniqueness.ts ├── database │ ├── methods │ │ ├── create │ │ │ ├── index.ts │ │ │ └── module.ts │ │ ├── drop │ │ │ ├── index.ts │ │ │ └── module.ts │ │ ├── check │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ └── module.ts │ │ ├── index.ts │ │ └── type.ts │ ├── index.ts │ ├── driver │ │ ├── utils │ │ │ ├── index.ts │ │ │ ├── charset.ts │ │ │ ├── character-set.ts │ │ │ ├── create.ts │ │ │ └── build.ts │ │ ├── index.ts │ │ ├── types.ts │ │ ├── sqlite.ts │ │ ├── mongodb.ts │ │ ├── oracle.ts │ │ ├── cockroachdb.ts │ │ ├── mssql.ts │ │ └── mysql.ts │ └── utils │ │ ├── index.ts │ │ ├── query.ts │ │ ├── type.ts │ │ ├── schema.ts │ │ ├── context.ts │ │ └── migration.ts ├── cli │ ├── commands │ │ ├── index.ts │ │ ├── seed │ │ │ ├── index.ts │ │ │ ├── run.ts │ │ │ └── create.ts │ │ └── database │ │ │ ├── index.ts │ │ │ ├── drop.ts │ │ │ └── create.ts │ └── index.ts ├── data-source │ ├── find │ │ ├── index.ts │ │ ├── type.ts │ │ └── module.ts │ ├── options │ │ ├── utils │ │ │ ├── index.ts │ │ │ ├── merge.ts │ │ │ └── env.ts │ │ ├── index.ts │ │ ├── type.ts │ │ ├── singleton.ts │ │ └── module.ts │ ├── index.ts │ ├── type.ts │ └── singleton.ts ├── errors │ ├── base.ts │ ├── index.ts │ ├── driver.ts │ └── options.ts ├── utils │ ├── tsconfig │ │ ├── index.ts │ │ ├── module.ts │ │ └── type.ts │ ├── code-transformation │ │ ├── index.ts │ │ ├── constants.ts │ │ └── module.ts │ ├── object.ts │ ├── file-system.ts │ ├── index.ts │ ├── promise.ts │ ├── has-property.ts │ ├── entity.ts │ ├── slash.ts │ └── separator.ts ├── query │ ├── parameter │ │ ├── fields │ │ │ ├── index.ts │ │ │ ├── type.ts │ │ │ └── module.ts │ │ ├── sort │ │ │ ├── index.ts │ │ │ ├── type.ts │ │ │ └── module.ts │ │ ├── filters │ │ │ ├── index.ts │ │ │ └── type.ts │ │ ├── pagination │ │ │ ├── index.ts │ │ │ ├── type.ts │ │ │ └── module.ts │ │ ├── relations │ │ │ ├── index.ts │ │ │ ├── type.ts │ │ │ └── module.ts │ │ └── index.ts │ ├── utils │ │ ├── index.ts │ │ ├── key.ts │ │ ├── alias.ts │ │ └── option.ts │ ├── index.ts │ ├── type.ts │ └── module.ts ├── env │ ├── index.ts │ ├── type.ts │ ├── utils.ts │ └── constants.ts ├── seeder │ ├── utils │ │ ├── index.ts │ │ ├── template.ts │ │ ├── file-path.ts │ │ └── prepare.ts │ ├── factory │ │ ├── index.ts │ │ ├── type.ts │ │ ├── manager.ts │ │ └── utils.ts │ ├── index.ts │ ├── module.ts │ ├── type.ts │ └── entity.ts └── index.ts ├── .release-please-manifest.json ├── commitlint.config.js ├── .husky └── commit-msg ├── docs ├── guide │ ├── installation.md │ ├── migration-guide-v3.md │ ├── index.md │ ├── instances.md │ ├── cli.md │ └── database.md ├── index.md └── .vitepress │ └── config.mjs ├── SECURITY.md ├── .gitignore ├── test ├── data │ ├── typeorm │ │ ├── data-source.ts │ │ ├── data-source-default.ts │ │ ├── ormconfig.json │ │ ├── data-source-async.ts │ │ ├── factory.ts │ │ ├── utils.ts │ │ └── FakeSelectQueryBuilder.ts │ ├── entity │ │ ├── role.ts │ │ └── user.ts │ ├── factory │ │ ├── role.ts │ │ └── user.ts │ ├── seed │ │ ├── role.ts │ │ └── user.ts │ └── tsconfig.json ├── unit │ ├── query │ │ ├── pagination.spec.ts │ │ ├── fields.spec.ts │ │ ├── sort.spec.ts │ │ ├── relations.spec.ts │ │ └── index.spec.ts │ ├── utils │ │ └── tsconfig.spec.ts │ ├── database │ │ ├── index.spec.ts │ │ └── migration.spec.ts │ ├── data-source │ │ ├── options │ │ │ ├── module.spec.ts │ │ │ ├── merge.spec.ts │ │ │ └── env.spec.ts │ │ ├── singleton.ts │ │ └── find.spec.ts │ ├── helper │ │ ├── entity-uniqueness.spec.ts │ │ ├── entity-property-names.spec.ts │ │ └── entity-join-columns.spec.ts │ ├── seeder │ │ ├── utils │ │ │ └── file-path.spec.ts │ │ ├── factory.spec.ts │ │ └── seeder.spec.ts │ └── env │ │ └── module.spec.ts └── jest.config.js ├── .editorconfig ├── tsconfig.eslint.json ├── release-please-config.json ├── tsconfig.json ├── .eslintrc ├── LICENSE ├── CONTRIBUTING.md ├── CODE_OF_CONDUCT.md └── package.json /CNAME: -------------------------------------------------------------------------------- 1 | typeorm-extension.tada5hi.net -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [tada5hi] 2 | -------------------------------------------------------------------------------- /writable/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | */ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './entity'; 2 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "3.7.3" 3 | } 4 | -------------------------------------------------------------------------------- /src/database/methods/create/index.ts: -------------------------------------------------------------------------------- 1 | export * from './module'; 2 | -------------------------------------------------------------------------------- /src/database/methods/drop/index.ts: -------------------------------------------------------------------------------- 1 | export * from './module'; 2 | -------------------------------------------------------------------------------- /src/cli/commands/index.ts: -------------------------------------------------------------------------------- 1 | export * from './database'; 2 | export * from './seed'; 3 | -------------------------------------------------------------------------------- /src/cli/commands/seed/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create'; 2 | export * from './run'; 3 | -------------------------------------------------------------------------------- /src/data-source/find/index.ts: -------------------------------------------------------------------------------- 1 | export * from './module'; 2 | export * from './type'; 3 | -------------------------------------------------------------------------------- /src/errors/base.ts: -------------------------------------------------------------------------------- 1 | export class TypeormExtensionError extends Error { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/tsconfig/index.ts: -------------------------------------------------------------------------------- 1 | export * from './module'; 2 | export * from './type'; 3 | -------------------------------------------------------------------------------- /src/cli/commands/database/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create'; 2 | export * from './drop'; 3 | -------------------------------------------------------------------------------- /src/query/parameter/fields/index.ts: -------------------------------------------------------------------------------- 1 | export * from './module'; 2 | export * from './type'; 3 | -------------------------------------------------------------------------------- /src/query/parameter/sort/index.ts: -------------------------------------------------------------------------------- 1 | export * from './module'; 2 | export * from './type'; 3 | -------------------------------------------------------------------------------- /src/data-source/options/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './env'; 2 | export * from './merge'; 3 | -------------------------------------------------------------------------------- /src/database/methods/check/index.ts: -------------------------------------------------------------------------------- 1 | export * from './module'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /src/query/parameter/filters/index.ts: -------------------------------------------------------------------------------- 1 | export * from './module'; 2 | export * from './type'; 3 | -------------------------------------------------------------------------------- /src/query/parameter/pagination/index.ts: -------------------------------------------------------------------------------- 1 | export * from './module'; 2 | export * from './type'; 3 | -------------------------------------------------------------------------------- /src/query/parameter/relations/index.ts: -------------------------------------------------------------------------------- 1 | export * from './module'; 2 | export * from './type'; 3 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@tada5hi/commitlint-config'], 3 | }; 4 | -------------------------------------------------------------------------------- /src/utils/code-transformation/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants'; 2 | export * from './module'; 3 | -------------------------------------------------------------------------------- /src/env/index.ts: -------------------------------------------------------------------------------- 1 | export * from './constants'; 2 | export * from './module'; 3 | export * from './type'; 4 | -------------------------------------------------------------------------------- /src/errors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base'; 2 | export * from './driver'; 3 | export * from './options'; 4 | -------------------------------------------------------------------------------- /src/database/index.ts: -------------------------------------------------------------------------------- 1 | export * from './methods'; 2 | export * from './driver'; 3 | export * from './utils'; 4 | -------------------------------------------------------------------------------- /src/query/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './alias'; 2 | export * from './key'; 3 | export * from './option'; 4 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit 5 | -------------------------------------------------------------------------------- /src/seeder/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './file-path'; 2 | export * from './prepare'; 3 | export * from './template'; 4 | -------------------------------------------------------------------------------- /src/query/index.ts: -------------------------------------------------------------------------------- 1 | export * from './module'; 2 | export * from './parameter'; 3 | export * from './type'; 4 | export * from './utils'; 5 | -------------------------------------------------------------------------------- /src/utils/code-transformation/constants.ts: -------------------------------------------------------------------------------- 1 | export enum CodeTransformation { 2 | JUST_IN_TIME = 'jit', 3 | NONE = 'none', 4 | } 5 | -------------------------------------------------------------------------------- /src/data-source/index.ts: -------------------------------------------------------------------------------- 1 | export * from './find'; 2 | export * from './options'; 3 | export * from './singleton'; 4 | export * from './type'; 5 | -------------------------------------------------------------------------------- /src/database/methods/index.ts: -------------------------------------------------------------------------------- 1 | export * from './check'; 2 | export * from './create'; 3 | export * from './drop'; 4 | export * from './type'; 5 | -------------------------------------------------------------------------------- /src/seeder/factory/index.ts: -------------------------------------------------------------------------------- 1 | export * from './manager'; 2 | export * from './module'; 3 | export * from './type'; 4 | export * from './utils'; 5 | -------------------------------------------------------------------------------- /src/data-source/options/index.ts: -------------------------------------------------------------------------------- 1 | export * from './module'; 2 | export * from './singleton'; 3 | export * from './type'; 4 | export * from './utils'; 5 | -------------------------------------------------------------------------------- /src/data-source/type.ts: -------------------------------------------------------------------------------- 1 | import type { DataSourceOptions } from 'typeorm'; 2 | 3 | export type DataSourceCacheOption = DataSourceOptions['cache']; 4 | -------------------------------------------------------------------------------- /src/database/driver/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './build'; 2 | export * from './character-set'; 3 | export * from './charset'; 4 | export * from './create'; 5 | -------------------------------------------------------------------------------- /docs/guide/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | Add the package as a dependency to the project. 4 | 5 | ```sh 6 | npm install typeorm-extension --save 7 | ``` 8 | -------------------------------------------------------------------------------- /src/database/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './context'; 2 | export * from './migration'; 3 | export * from './query'; 4 | export * from './schema'; 5 | export * from './type'; 6 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | ## Reporting a Vulnerability 3 | 4 | If you discover a security vulnerability regarding this project, please e-mail me to contact@tada5hi.net! 5 | -------------------------------------------------------------------------------- /src/query/parameter/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fields'; 2 | export * from './filters'; 3 | export * from './pagination'; 4 | export * from './relations'; 5 | export * from './sort'; 6 | -------------------------------------------------------------------------------- /src/helpers/entity/index.ts: -------------------------------------------------------------------------------- 1 | export * from './error'; 2 | export * from './property-names'; 3 | export * from './metadata'; 4 | export * from './join-columns'; 5 | export * from './uniqueness'; 6 | -------------------------------------------------------------------------------- /src/query/utils/key.ts: -------------------------------------------------------------------------------- 1 | export function buildKeyWithPrefix(name: string, prefix?: string) { 2 | if (prefix) { 3 | return `${prefix}.${name}`; 4 | } 5 | 6 | return name; 7 | } 8 | -------------------------------------------------------------------------------- /src/seeder/index.ts: -------------------------------------------------------------------------------- 1 | export * from './entity'; 2 | export * from './executor'; 3 | export * from './factory'; 4 | export * from './module'; 5 | export * from './utils'; 6 | export * from './type'; 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | bin 3 | dist 4 | doc 5 | node_modules 6 | .vscode 7 | .nyc_output 8 | coverage 9 | *.log 10 | npm-debug.log* 11 | .idea 12 | reports 13 | db.sqlite 14 | .env 15 | cache 16 | -------------------------------------------------------------------------------- /test/data/typeorm/data-source.ts: -------------------------------------------------------------------------------- 1 | import { DataSource } from 'typeorm'; 2 | import { createDataSourceOptions } from './factory'; 3 | 4 | export const options = createDataSourceOptions(); 5 | 6 | export const dataSource = new DataSource(options); 7 | -------------------------------------------------------------------------------- /src/query/parameter/pagination/type.ts: -------------------------------------------------------------------------------- 1 | import type { PaginationParseOptions, PaginationParseOutput } from 'rapiq'; 2 | 3 | export type QueryPaginationApplyOptions = PaginationParseOptions; 4 | export type QueryPaginationApplyOutput = PaginationParseOutput; 5 | -------------------------------------------------------------------------------- /test/data/entity/role.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; 2 | 3 | @Entity() 4 | export class Role { 5 | @PrimaryGeneratedColumn() 6 | id: number; 7 | 8 | @Column() 9 | name: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/object.ts: -------------------------------------------------------------------------------- 1 | export function pickRecord(data: Record, keys: string[]) { 2 | const output : Record = {}; 3 | for (let i = 0; i < keys.length; i++) { 4 | output[keys[i]] = data[keys[i]]; 5 | } 6 | 7 | return output; 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: ❗️ All other issues 4 | url: https://github.com/Tada5hi/typeorm-extension/discussions 5 | about: | 6 | Please use GitHub Discussions for other issues and asking questions. 7 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './errors'; 2 | export * from './query'; 3 | export * from './cli/commands'; 4 | export * from './database'; 5 | export * from './data-source'; 6 | export * from './env'; 7 | export * from './helpers'; 8 | export * from './seeder'; 9 | export * from './utils'; 10 | -------------------------------------------------------------------------------- /src/database/driver/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cockroachdb'; 2 | export * from './mongodb'; 3 | export * from './mssql'; 4 | export * from './mysql'; 5 | export * from './oracle'; 6 | export * from './postgres'; 7 | export * from './sqlite'; 8 | export * from './types'; 9 | export * from './utils'; 10 | -------------------------------------------------------------------------------- /src/utils/file-system.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | 3 | export async function isDirectory(input: string) : Promise { 4 | try { 5 | const stat = await fs.promises.stat(input); 6 | return stat.isDirectory(); 7 | } catch (e) { 8 | return false; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/data/factory/role.ts: -------------------------------------------------------------------------------- 1 | import { setSeederFactory } from '../../../src'; 2 | import { Role } from '../entity/role'; 3 | 4 | export default setSeederFactory(Role, async (faker) => { 5 | const role = new Role(); 6 | role.name = faker.person.firstName('female'); 7 | 8 | return role; 9 | }); 10 | -------------------------------------------------------------------------------- /src/query/parameter/sort/type.ts: -------------------------------------------------------------------------------- 1 | import type { SortParseOptions, SortParseOutput } from 'rapiq'; 2 | import type { ObjectLiteral } from 'typeorm'; 3 | 4 | export type QuerySortApplyOptions = SortParseOptions & { 5 | defaultAlias?: string 6 | }; 7 | export type QuerySortApplyOutput = SortParseOutput; 8 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './code-transformation'; 2 | export * from './entity'; 3 | export * from './file-path'; 4 | export * from './file-system'; 5 | export * from './has-property'; 6 | export * from './object'; 7 | export * from './promise'; 8 | export * from './separator'; 9 | export * from './slash'; 10 | export * from './tsconfig'; 11 | -------------------------------------------------------------------------------- /src/database/utils/query.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore next */ 2 | import type { ObjectLiteral } from 'rapiq'; 3 | import type { SelectQueryBuilder } from 'typeorm'; 4 | 5 | export function existsQuery(builder: SelectQueryBuilder, inverse = false) { 6 | return `${inverse ? 'not ' : ''}exists (${builder.getQuery()})`; 7 | } 8 | -------------------------------------------------------------------------------- /test/data/typeorm/data-source-default.ts: -------------------------------------------------------------------------------- 1 | import type { DataSourceOptions } from 'typeorm'; 2 | import { DataSource } from 'typeorm'; 3 | import path from 'path'; 4 | 5 | export const options: DataSourceOptions = { 6 | type: 'better-sqlite3', 7 | database: path.join(__dirname, 'db.sqlite'), 8 | }; 9 | 10 | export default new DataSource(options); 11 | -------------------------------------------------------------------------------- /src/query/parameter/relations/type.ts: -------------------------------------------------------------------------------- 1 | import type { RelationsParseOptions, RelationsParseOutput } from 'rapiq'; 2 | import type { ObjectLiteral } from 'typeorm'; 3 | 4 | export type QueryRelationsApplyOptions = RelationsParseOptions & { 5 | defaultAlias?: string 6 | }; 7 | export type QueryRelationsApplyOutput = RelationsParseOutput; 8 | -------------------------------------------------------------------------------- /test/data/typeorm/ormconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "skip": false, 3 | "type": "mysql", 4 | "host": "localhost", 5 | "port": 3306, 6 | "username": "root", 7 | "password": "admin", 8 | "database": "test", 9 | "logging": false, 10 | "extra": { 11 | "socketPath": "/var/mysqld/mysqld.sock", 12 | "charset": "UTF8_GENERAL_CI" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true 6 | }, 7 | "include": [ 8 | "commitlint.config.js", 9 | "release.config.js", 10 | "src/**/*.ts", 11 | "test/**/*.ts", 12 | "test/**/*.js", 13 | "test/**/*.spec.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/query/type.ts: -------------------------------------------------------------------------------- 1 | import type { ParseOptions, ParseOutput } from 'rapiq'; 2 | import type { ObjectLiteral } from 'typeorm'; 3 | 4 | export type QueryApplyOptions< 5 | T extends ObjectLiteral = ObjectLiteral, 6 | > = ParseOptions & { 7 | defaultAlias?: string 8 | }; 9 | 10 | export type QueryApplyOutput = ParseOutput & { 11 | defaultAlias?: string 12 | }; 13 | -------------------------------------------------------------------------------- /src/utils/promise.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from 'locter'; 2 | 3 | export function isPromise(p: unknown): p is Promise { 4 | return isObject(p) && 5 | ( 6 | p instanceof Promise || 7 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 8 | // @ts-ignore 9 | typeof p.then === 'function' 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/query/parameter/fields/type.ts: -------------------------------------------------------------------------------- 1 | import type { FieldsParseOptions, FieldsParseOutput } from 'rapiq'; 2 | import type { ObjectLiteral } from 'typeorm'; 3 | 4 | export type QueryFieldsApplyOptions< 5 | T extends ObjectLiteral = ObjectLiteral, 6 | > = FieldsParseOptions & { 7 | defaultAlias?: string 8 | }; 9 | export type QueryFieldsApplyOutput = FieldsParseOutput; 10 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: home 3 | hero: 4 | name: typeorm-extension 5 | tagline: A library to create, drop & seed the database and apply URL query parameter(s). 6 | actions: 7 | - theme: brand 8 | text: Get Started 9 | link: /guide/ 10 | - theme: alt 11 | text: View on GitHub 12 | link: https://github.com/tada5hi/typeorm-extension 13 | --- 14 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "include-component-in-tag": false, 3 | "prerelease": true, 4 | "prerelease-type": "alpha", 5 | "release-type": "node", 6 | "bump-minor-pre-major": true, 7 | "bump-patch-for-minor-pre-major": true, 8 | "packages": { 9 | ".": { } 10 | }, 11 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" 12 | } 13 | -------------------------------------------------------------------------------- /src/utils/has-property.ts: -------------------------------------------------------------------------------- 1 | export function hasOwnProperty, Y extends PropertyKey>(obj: X, prop: Y): obj is X & Record { 2 | return Object.prototype.hasOwnProperty.call(obj, prop); 3 | } 4 | 5 | export function hasStringProperty, Y extends PropertyKey>(obj: X, prop: Y): obj is X & Record { 6 | return hasOwnProperty(obj, prop) && 7 | typeof obj[prop] === 'string'; 8 | } 9 | -------------------------------------------------------------------------------- /test/data/factory/user.ts: -------------------------------------------------------------------------------- 1 | import { setSeederFactory } from '../../../src'; 2 | import { User } from '../entity/user'; 3 | 4 | export default setSeederFactory(User, async (faker) => { 5 | const user = new User(); 6 | user.firstName = faker.person.firstName('male'); 7 | user.lastName = faker.person.lastName('male'); 8 | user.email = faker.internet.email({ firstName: user.firstName, lastName: user.lastName }); 9 | 10 | return user; 11 | }); 12 | -------------------------------------------------------------------------------- /src/utils/entity.ts: -------------------------------------------------------------------------------- 1 | import type { EntitySchema, ObjectType } from 'typeorm'; 2 | import { InstanceChecker } from 'typeorm'; 3 | 4 | export function getEntityName(entity: ObjectType | EntitySchema) : string { 5 | if (typeof entity === 'function') { 6 | return entity.name; 7 | } 8 | 9 | if (InstanceChecker.isEntitySchema(entity)) { 10 | return entity.options.name; 11 | } 12 | 13 | return new (entity as any)().constructor.name; 14 | } 15 | -------------------------------------------------------------------------------- /src/query/utils/alias.ts: -------------------------------------------------------------------------------- 1 | import type { QueryRelationsApplyOutput } from '../parameter'; 2 | 3 | export function getAliasForPath(items?: QueryRelationsApplyOutput, path?: string) { 4 | if (typeof path === 'undefined' || typeof items === 'undefined') { 5 | return undefined; 6 | } 7 | 8 | for (let i = 0; i < items.length; i++) { 9 | if (items[i].key === path) { 10 | return items[i].value; 11 | } 12 | } 13 | 14 | return undefined; 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tada5hi/tsconfig", 3 | "compilerOptions": { 4 | "lib": [ 5 | "ESNext" 6 | ], 7 | "module": "ESNext", 8 | "outDir": "dist", 9 | "target": "ES2022", 10 | "experimentalDecorators": true, 11 | "emitDecoratorMetadata": true, 12 | "strictPropertyInitialization": false 13 | }, 14 | "include": [ 15 | "src/**/*.ts" 16 | ], 17 | "exclude": [ 18 | "node_modules/**/*" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/errors/driver.ts: -------------------------------------------------------------------------------- 1 | import { TypeormExtensionError } from './base'; 2 | 3 | export class DriverError extends TypeormExtensionError { 4 | constructor(message?: string) { 5 | super(message || 'A database driver related error has occurred.'); 6 | } 7 | 8 | static undeterminable() { 9 | return new DriverError('The driver could not be determined.'); 10 | } 11 | 12 | static notSupported(driverName: string) { 13 | return new DriverError(`The driver ${driverName} is not supported yet.`); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/query/utils/option.ts: -------------------------------------------------------------------------------- 1 | import { hasOwnProperty } from '../../utils'; 2 | 3 | export function isQueryOptionDefined( 4 | input: Record | boolean, 5 | option: string | string[], 6 | ) { 7 | if (typeof input === 'boolean') { 8 | return false; 9 | } 10 | 11 | const options = Array.isArray(option) ? option : [option]; 12 | 13 | for (let i = 0; i < options.length; i++) { 14 | if (hasOwnProperty(input, options[i])) { 15 | return true; 16 | } 17 | } 18 | 19 | return false; 20 | } 21 | -------------------------------------------------------------------------------- /src/database/driver/utils/charset.ts: -------------------------------------------------------------------------------- 1 | import type { DataSourceOptions } from 'typeorm'; 2 | import { hasOwnProperty } from '../../../utils'; 3 | 4 | export function getCharsetFromDataSourceOptions(options: DataSourceOptions) : string | undefined { 5 | if ( 6 | hasOwnProperty(options, 'charset') && 7 | typeof options.charset === 'string' 8 | ) { 9 | return options.charset; 10 | } 11 | 12 | if (typeof options?.extra?.charset === 'string') { 13 | return options.extra.charset; 14 | } 15 | 16 | return undefined; 17 | } 18 | -------------------------------------------------------------------------------- /test/data/typeorm/data-source-async.ts: -------------------------------------------------------------------------------- 1 | import type { DataSource } from 'typeorm'; 2 | import { dataSource } from './data-source'; 3 | 4 | let instance : DataSource | undefined; 5 | let instancePromise : Promise | undefined; 6 | 7 | export async function useDataSource() { 8 | if (typeof instance !== 'undefined') { 9 | return instance; 10 | } 11 | 12 | if (typeof instancePromise === 'undefined') { 13 | instancePromise = dataSource.initialize(); 14 | } 15 | 16 | instance = await instancePromise; 17 | 18 | return instance; 19 | } 20 | -------------------------------------------------------------------------------- /src/seeder/factory/type.ts: -------------------------------------------------------------------------------- 1 | import type { Faker } from '@faker-js/faker'; 2 | import type { EntitySchema, ObjectType } from 'typeorm'; 3 | 4 | export type FactoryCallback = (faker: Faker, meta: Meta) => O | Promise; 5 | 6 | export type SeederFactoryItem = { 7 | factoryFn: FactoryCallback, 8 | entity: ObjectType | EntitySchema 9 | }; 10 | 11 | export type SeederFactoryContext = { 12 | name: string, 13 | entity: ObjectType | EntitySchema, 14 | factoryFn: FactoryCallback 15 | }; 16 | -------------------------------------------------------------------------------- /src/utils/code-transformation/module.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import { CodeTransformation } from './constants'; 3 | 4 | export function detectCodeTransformation() : `${CodeTransformation}` { 5 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 6 | // @ts-ignore 7 | if (process[Symbol.for('ts-node.register.instance')]) { 8 | return CodeTransformation.JUST_IN_TIME; 9 | } 10 | 11 | return CodeTransformation.NONE; 12 | } 13 | 14 | export function isCodeTransformation(input: string) { 15 | return detectCodeTransformation() === input; 16 | } 17 | -------------------------------------------------------------------------------- /src/database/driver/utils/character-set.ts: -------------------------------------------------------------------------------- 1 | import type { DataSourceOptions } from 'typeorm'; 2 | import { hasOwnProperty } from '../../../utils'; 3 | 4 | export function getCharacterSetFromDataSourceOptions(options: DataSourceOptions) : string | undefined { 5 | if ( 6 | hasOwnProperty(options, 'characterSet') && 7 | typeof options.characterSet === 'string' 8 | ) { 9 | return options.characterSet; 10 | } 11 | 12 | if (typeof options?.extra?.characterSet === 'string') { 13 | return options.extra.characterSet; 14 | } 15 | 16 | return undefined; 17 | } 18 | -------------------------------------------------------------------------------- /.github/actions/build/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Build' 2 | description: 'Prepares the repo for a job by running the build' 3 | 4 | runs: 5 | using: 'composite' 6 | steps: 7 | - name: Use cache 8 | id: 'cache' 9 | uses: actions/cache@v3 10 | with: 11 | path: | 12 | **/dist/** 13 | **/bin/** 14 | key: ${{ runner.os }}-build-${{ github.sha }} 15 | 16 | - name: Build 17 | shell: bash 18 | if: steps.cache.outputs.cache-hit != 'true' 19 | run: | 20 | npm run build 21 | -------------------------------------------------------------------------------- /src/seeder/utils/template.ts: -------------------------------------------------------------------------------- 1 | import { pascalCase } from 'pascal-case'; 2 | 3 | export function buildSeederFileTemplate( 4 | name: string, 5 | timestamp: number, 6 | ): string { 7 | const className = `${pascalCase(name)}${timestamp}`; 8 | 9 | return `import { DataSource } from 'typeorm'; 10 | import { Seeder, SeederFactoryManager } from 'typeorm-extension'; 11 | 12 | export class ${className} implements Seeder { 13 | track = false; 14 | 15 | public async run( 16 | dataSource: DataSource, 17 | factoryManager: SeederFactoryManager 18 | ): Promise { 19 | 20 | } 21 | } 22 | `; 23 | } 24 | -------------------------------------------------------------------------------- /test/unit/query/pagination.spec.ts: -------------------------------------------------------------------------------- 1 | import { applyPagination, applyQueryPagination } from '../../../src'; 2 | import { FakeSelectQueryBuilder } from '../../data/typeorm/FakeSelectQueryBuilder'; 3 | 4 | describe('src/api/pagination.ts', () => { 5 | it('should apply pagination', () => { 6 | const query = new FakeSelectQueryBuilder(); 7 | let value = applyQueryPagination(query, undefined, { maxLimit: 50 }); 8 | expect(value).toEqual({ offset: 0, limit: 50 }); 9 | 10 | value = applyPagination(query, undefined, { maxLimit: 50 }); 11 | expect(value).toEqual({ offset: 0, limit: 50 }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@tada5hi/eslint-config-typescript" 4 | ], 5 | "parserOptions": { 6 | "project": "./tsconfig.eslint.json" 7 | }, 8 | "ignorePatterns": ["**/dist/*", "**/*.d.ts"], 9 | "globals": { 10 | "NodeJS": true 11 | }, 12 | "rules": { 13 | "class-methods-use-this": "off", 14 | "import/no-cycle": [2, { 15 | "maxDepth": 1 16 | }], 17 | "no-shadow": "off", 18 | "no-use-before-define": "off", 19 | 20 | "@typescript-eslint/no-unused-vars": "off", 21 | "@typescript-eslint/no-use-before-define": "off" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/data/seed/role.ts: -------------------------------------------------------------------------------- 1 | import type { DataSource } from 'typeorm'; 2 | import type { Seeder, SeederFactoryManager } from '../../../src'; 3 | import { Role } from '../entity/role'; 4 | 5 | export class RoleSeeder implements Seeder { 6 | track = true; 7 | 8 | public async run( 9 | dataSource: DataSource, 10 | _factoryManager: SeederFactoryManager, 11 | ) : Promise { 12 | const repository = dataSource.getRepository(Role); 13 | 14 | await repository.insert([ 15 | { 16 | name: 'admin', 17 | }, 18 | ]); 19 | 20 | return undefined; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/data-source/options/utils/merge.ts: -------------------------------------------------------------------------------- 1 | import { createMerger } from 'smob'; 2 | import type { DataSourceOptions } from 'typeorm'; 3 | 4 | const merge = createMerger({ 5 | strategy: (target, key, value) => { 6 | if (typeof target[key] === 'undefined') { 7 | target[key] = value; 8 | 9 | return target; 10 | } 11 | 12 | return undefined; 13 | }, 14 | }); 15 | 16 | export function mergeDataSourceOptions( 17 | target: DataSourceOptions, 18 | source: DataSourceOptions, 19 | ) { 20 | if (target.type !== source.type) { 21 | return target; 22 | } 23 | 24 | return merge(target, source); 25 | } 26 | -------------------------------------------------------------------------------- /test/unit/query/fields.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | applyFields, 3 | applyQueryFields, 4 | } from '../../../src'; 5 | 6 | import { FakeSelectQueryBuilder } from '../../data/typeorm/FakeSelectQueryBuilder'; 7 | 8 | describe('src/api/fields.ts', () => { 9 | it('should apply query fields', () => { 10 | const queryBuilder = new FakeSelectQueryBuilder(); 11 | 12 | let value = applyQueryFields(queryBuilder, [], {}); 13 | expect(value).toBeDefined(); 14 | expect(value).toEqual([]); 15 | 16 | value = applyFields(queryBuilder, [], {}); 17 | expect(value).toBeDefined(); 18 | expect(value).toEqual([]); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/errors/options.ts: -------------------------------------------------------------------------------- 1 | import { TypeormExtensionError } from './base'; 2 | 3 | export class OptionsError extends TypeormExtensionError { 4 | constructor(message?: string) { 5 | super(message || 'A database options related error has occurred'); 6 | } 7 | 8 | static undeterminable() { 9 | return new OptionsError('The database options could not be determined.'); 10 | } 11 | 12 | static notFound() { 13 | return new OptionsError('The database options could not be located/loaded.'); 14 | } 15 | 16 | static databaseNotDefined() { 17 | return new OptionsError('The database name to connect to is not defined.'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/query/parameter/filters/type.ts: -------------------------------------------------------------------------------- 1 | import type { FiltersParseOptions, FiltersParseOutput } from 'rapiq'; 2 | import type { ObjectLiteral } from 'typeorm'; 3 | 4 | export type QueryFiltersApplyOptions< 5 | T extends ObjectLiteral = ObjectLiteral, 6 | > = FiltersParseOptions & { 7 | bindingKey?: (key: string) => string, 8 | defaultAlias?: string 9 | }; 10 | 11 | export type QueryFiltersApplyOutput = FiltersParseOutput; 12 | 13 | // ----------------------------------------- 14 | 15 | export type QueryFiltersOutputElement = { 16 | statement: string, 17 | binding: Record 18 | }; 19 | export type QueryFiltersOutput = QueryFiltersOutputElement[]; 20 | -------------------------------------------------------------------------------- /test/data/entity/user.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Column, Entity, JoinColumn, ManyToOne, 3 | PrimaryGeneratedColumn, Unique, 4 | } from 'typeorm'; 5 | import { Role } from './role'; 6 | 7 | @Unique(['firstName', 'lastName']) 8 | @Entity() 9 | export class User { 10 | @PrimaryGeneratedColumn() 11 | id: number; 12 | 13 | @Column() 14 | firstName: string; 15 | 16 | @Column() 17 | lastName: string; 18 | 19 | @Column() 20 | email: string; 21 | 22 | @Column({ nullable: true }) 23 | roleId: number | null; 24 | 25 | @ManyToOne(() => Role, (role: Role) => role.id, { nullable: true }) 26 | @JoinColumn({ name: 'roleId' }) 27 | role: Role; 28 | } 29 | -------------------------------------------------------------------------------- /src/utils/slash.ts: -------------------------------------------------------------------------------- 1 | const TRAILING_SLASH_RE = /\/$|\/\?/; 2 | 3 | export function hasTrailingSlash(input = '', queryParams = false): boolean { 4 | if (!queryParams) { 5 | return input.endsWith('/'); 6 | } 7 | 8 | return TRAILING_SLASH_RE.test(input); 9 | } 10 | 11 | export function withoutTrailingSlash(input = '', queryParams = false): string { 12 | if (!queryParams) { 13 | return (hasTrailingSlash(input) ? input.slice(0, -1) : input) || '/'; 14 | } 15 | 16 | if (!hasTrailingSlash(input, true)) { 17 | return input || '/'; 18 | } 19 | 20 | const [s0, ...s] = input.split('?'); 21 | 22 | return (s0.slice(0, -1) || '/') + (s.length ? `?${s.join('?')}` : ''); 23 | } 24 | -------------------------------------------------------------------------------- /src/data-source/find/type.ts: -------------------------------------------------------------------------------- 1 | import type { TSConfig } from '../../utils/tsconfig'; 2 | 3 | export type DataSourceFindOptions = { 4 | /** 5 | * Directory where to look for DataSource files. 6 | */ 7 | directory?: string, 8 | 9 | /** 10 | * DataSource file name. 11 | */ 12 | fileName?: string, 13 | 14 | /** 15 | * This option indicates if file paths should be preserved, 16 | * and treated as if the just-in-time compilation environment is detected. 17 | */ 18 | preserveFilePaths?: boolean, 19 | 20 | /** 21 | * Directory path to the tsconfig.json file 22 | * 23 | * Default: process.cwd() + path.sep + tsconfig.json 24 | */ 25 | tsconfig?: string | TSConfig, 26 | }; 27 | -------------------------------------------------------------------------------- /src/utils/tsconfig/module.ts: -------------------------------------------------------------------------------- 1 | import { isObject, load } from 'locter'; 2 | import path from 'node:path'; 3 | import type { TSConfig } from './type'; 4 | 5 | export async function readTSConfig(input?: string) : Promise { 6 | input = input || process.cwd(); 7 | input = path.isAbsolute(input) ? 8 | input : 9 | path.resolve(process.cwd(), input); 10 | 11 | const filePath = input.indexOf('.json') === -1 ? 12 | path.join(input, 'tsconfig.json') : 13 | input; 14 | 15 | try { 16 | const tsConfig = await load(filePath); 17 | 18 | if (isObject(tsConfig)) { 19 | return tsConfig; 20 | } 21 | } catch (e) { 22 | // don't do anything ;) 23 | } 24 | 25 | return {}; 26 | } 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🚨 Bug report 3 | about: Report a bug report, to improve this project. 4 | title: 'Bug: ' 5 | labels: 'bug-report' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 16 | 17 | ### Versions 18 | - Node: 19 | - OS: 20 | 21 | ### Reproduction 22 | 23 |
24 | Additional Details 25 |
26 | 27 | ### Steps to reproduce 28 | 29 | 30 | ### What is Expected? 31 | 32 | 33 | ### What is actually happening? 34 | -------------------------------------------------------------------------------- /src/cli/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'reflect-metadata'; 3 | import process from 'node:process'; 4 | import yargs from 'yargs'; 5 | import { hideBin } from 'yargs/helpers'; 6 | 7 | import { 8 | DatabaseCreateCommand, 9 | DatabaseDropCommand, 10 | SeedCreateCommand, 11 | SeedRunCommand, 12 | } from './commands'; 13 | 14 | yargs(hideBin(process.argv)) 15 | .scriptName('typeorm-extension') 16 | .usage('Usage: $0 [options]') 17 | .demandCommand(1) 18 | .command(new DatabaseCreateCommand()) 19 | .command(new DatabaseDropCommand()) 20 | .command(new SeedRunCommand()) 21 | .command(new SeedCreateCommand()) 22 | .strict() 23 | .alias('v', 'version') 24 | .help('h') 25 | .alias('h', 'help') 26 | .parse(); 27 | -------------------------------------------------------------------------------- /src/database/driver/types.ts: -------------------------------------------------------------------------------- 1 | export type DriverOptions = { 2 | database?: string, 3 | host?: string, 4 | user?: string, 5 | password?: string, 6 | port?: number, 7 | ssl?: any, 8 | 9 | // required for oracle and optional for other drivers 10 | url?: string, 11 | connectString?: string, 12 | sid?: string | number, 13 | serviceName?: string, 14 | 15 | // add mssql support 16 | domain?: string, 17 | 18 | charset?: string, 19 | characterSet?: string, 20 | 21 | // postgres specific 22 | schema?: string, 23 | 24 | // only for postgres 13+, see https://www.postgresql.org/docs/current/manage-ag-templatedbs.html 25 | template?: string, 26 | 27 | extra?: { 28 | [key: string]: any 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /src/database/driver/utils/create.ts: -------------------------------------------------------------------------------- 1 | import type { DataSource, DataSourceOptions } from 'typeorm'; 2 | import { DriverFactory } from 'typeorm/driver/DriverFactory'; 3 | 4 | const driversRequireDatabaseOption: DataSourceOptions['type'][] = [ 5 | 'sqlite', 6 | 'better-sqlite3', 7 | ]; 8 | 9 | export function createDriver(connectionOptions: DataSourceOptions) { 10 | const fakeConnection: DataSource = { 11 | options: { 12 | type: connectionOptions.type, 13 | ...(driversRequireDatabaseOption.indexOf(connectionOptions.type) !== -1 ? { 14 | database: connectionOptions.database, 15 | } : {}), 16 | }, 17 | } as DataSource; 18 | 19 | const driverFactory = new DriverFactory(); 20 | return driverFactory.create(fakeConnection); 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/tsconfig/type.ts: -------------------------------------------------------------------------------- 1 | import type { CompilerOptions, TypeAcquisition } from 'typescript'; 2 | 3 | export type StripEnums> = { 4 | [K in keyof T]: T[K] extends boolean 5 | ? T[K] 6 | : T[K] extends string 7 | ? T[K] 8 | : T[K] extends object 9 | ? T[K] 10 | : T[K] extends Array 11 | ? T[K] 12 | : T[K] extends undefined 13 | ? undefined 14 | : any; 15 | }; 16 | 17 | export interface TSConfig { 18 | compilerOptions?: StripEnums; 19 | exclude?: string[]; 20 | compileOnSave?: boolean; 21 | extends?: string; 22 | files?: string[]; 23 | include?: string[]; 24 | typeAcquisition?: TypeAcquisition; 25 | } 26 | -------------------------------------------------------------------------------- /test/data/typeorm/factory.ts: -------------------------------------------------------------------------------- 1 | import type { DataSourceOptions } from 'typeorm'; 2 | import { DataSource } from 'typeorm'; 3 | import type { SeederOptions } from '../../../src'; 4 | import { Role } from '../entity/role'; 5 | import { User } from '../entity/user'; 6 | 7 | export function createDataSourceOptions() : DataSourceOptions & SeederOptions { 8 | return { 9 | type: 'better-sqlite3', 10 | entities: [Role, User], 11 | database: ':memory:', 12 | factories: ['test/data/factory/**/*.{ts,.js}'], 13 | seeds: ['test/data/seed/**/*.{ts,js}'], 14 | extra: { 15 | charset: 'UTF8_GENERAL_CI', 16 | }, 17 | }; 18 | } 19 | 20 | export function createDataSource(options?: DataSourceOptions) : DataSource { 21 | return new DataSource(options || createDataSourceOptions()); 22 | } 23 | -------------------------------------------------------------------------------- /src/data-source/options/type.ts: -------------------------------------------------------------------------------- 1 | import type { TSConfig } from '../../utils/tsconfig'; 2 | 3 | export type DataSourceOptionsBuildContext = { 4 | /** 5 | * Data source file name without extension 6 | * 7 | * Default: data-source 8 | */ 9 | dataSourceName?: string, 10 | 11 | /** 12 | * Directory where to find dataSource + config 13 | * 14 | * Default: process.cwd() 15 | */ 16 | directory?: string, 17 | 18 | /** 19 | * Directory path to the tsconfig.json file 20 | * 21 | * Default: process.cwd() + path.sep + tsconfig.json 22 | */ 23 | tsconfig?: string | TSConfig, 24 | 25 | /** 26 | * This option indicates if file paths should be preserved, 27 | * and treated as if the just-in-time compilation environment is detected. 28 | */ 29 | preserveFilePaths?: boolean 30 | }; 31 | -------------------------------------------------------------------------------- /test/data/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "skipLibCheck": true, 4 | "esModuleInterop": true, 5 | "declaration": true, 6 | "experimentalDecorators": true, 7 | "emitDecoratorMetadata": true, 8 | "lib": [ 9 | "ESNext" 10 | ], 11 | "module": "commonjs", 12 | "newLine": "LF", 13 | "noFallthroughCasesInSwitch": true, 14 | "noImplicitAny": true, 15 | "noImplicitReturns": true, 16 | "noImplicitThis": false, 17 | "noUnusedParameters": false, 18 | "noUnusedLocals": true, 19 | "outDir": "output", 20 | "sourceMap": true, 21 | "strictNullChecks": false, 22 | "target": "es5" 23 | }, 24 | "include": [ 25 | "src/**/*.ts" 26 | ], 27 | "exclude": [ 28 | "node_modules/**/*" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /test/unit/utils/tsconfig.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { readTSConfig } from '../../../src/utils/tsconfig'; 3 | 4 | describe('src/utils/tsconfig.ts', () => { 5 | it('should read tsconfig file', async () => { 6 | const rootPath : string = path.resolve(__dirname, '..', '..', 'data'); 7 | const tsConfig = await readTSConfig(rootPath); 8 | 9 | expect(tsConfig.compilerOptions).toBeDefined(); 10 | if (tsConfig.compilerOptions) { 11 | expect(tsConfig.compilerOptions.outDir).toEqual('output'); 12 | } 13 | }); 14 | 15 | it('should read empty tsconfig', async () => { 16 | const rootPath : string = path.resolve(__dirname, '..', '..', 'data', 'foo'); 17 | const tsConfig = await readTSConfig(rootPath); 18 | 19 | expect(tsConfig).toBeDefined(); 20 | expect(tsConfig).toEqual({}); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/database/utils/type.ts: -------------------------------------------------------------------------------- 1 | import type { DataSource } from 'typeorm'; 2 | 3 | export type MigrationGenerateResult = { 4 | up: string[], 5 | down: string[], 6 | content?: string 7 | }; 8 | 9 | export type MigrationGenerateCommandContext = { 10 | /** 11 | * Directory where the migration(s) should be stored. 12 | */ 13 | directoryPath?: string, 14 | /** 15 | * Name of the migration class. 16 | */ 17 | name?: string, 18 | /** 19 | * DataSource used for reference of existing schema. 20 | */ 21 | dataSource: DataSource, 22 | 23 | /** 24 | * Timestamp in milliseconds. 25 | */ 26 | timestamp?: number, 27 | 28 | /** 29 | * Prettify sql statements. 30 | */ 31 | prettify?: boolean, 32 | 33 | /** 34 | * Only return up- & down-statements instead of backing up the migration to the file system. 35 | */ 36 | preview?: boolean 37 | }; 38 | -------------------------------------------------------------------------------- /test/unit/query/sort.spec.ts: -------------------------------------------------------------------------------- 1 | import { parseQuerySort } from 'rapiq'; 2 | import { applyQuerySort, applyQuerySortParseOutput, applySort } from '../../../src'; 3 | import { FakeSelectQueryBuilder } from '../../data/typeorm/FakeSelectQueryBuilder'; 4 | 5 | describe('src/api/sort.ts', () => { 6 | const query = new FakeSelectQueryBuilder(); 7 | it('should apply sort transformed', () => { 8 | let data = applyQuerySortParseOutput(query, parseQuerySort('id', { allowed: ['id'] })); 9 | expect(data).toBeDefined(); 10 | 11 | data = applyQuerySortParseOutput(query, []); 12 | expect(data).toEqual([]); 13 | }); 14 | 15 | it('should apply sort', () => { 16 | let applied = applyQuerySort(query, 'id', { allowed: ['id'] }); 17 | expect(applied).toBeDefined(); 18 | 19 | applied = applySort(query, 'id', { allowed: ['id'] }); 20 | expect(applied).toBeDefined(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/suggest-a-feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🧠 Feature request 3 | about: Suggest an idea or enhancement for this project 4 | title: 'Feature: ' 5 | labels: 'feature-request' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | ### Is your feature request related to a problem? Please describe. 13 | 14 | 15 | 16 | ### Describe the solution you'd like 17 | 18 | 19 | 20 | ### Describe alternatives you've considered 21 | 22 | 23 | 24 | ### Additional context 25 | 26 | 27 | -------------------------------------------------------------------------------- /docs/guide/migration-guide-v3.md: -------------------------------------------------------------------------------- 1 | # Upgrading to v3 2 | 3 | This the migration guide for upgrading from **v2** to **v3**. 4 | 5 | ## CLI 6 | 7 | ### File Path 8 | 9 | **Old** 10 | 11 | ```shell 12 | ts-node ./node_modules/typeorm-extension/dist/cli/index.js 13 | ``` 14 | 15 | **New** 16 | ```shell 17 | // CommonJS 18 | ts-node ./node_modules/typeorm-extension/bin/cli.cjs 19 | 20 | // ESM 21 | ts-node-esm ./node_modules/typeorm-extension/bin/cli.mjs 22 | ``` 23 | 24 | ### General 25 | 1. The seeding command **seed** has been renamed to **seed:run**. 26 | 2. The seeding option **--seed** has been renamed to **--name**. 27 | 3. The seeding option **--root** now corresponds to the root directory of the project. 28 | 4. The **--dataSource** option now can contain the relative path and the name to the data-source. 29 | 30 | ## DataSource 31 | 32 | 1. Drop support for **ormconfig**. The DataSource finder will no longer look for this kind of configuration. 33 | -------------------------------------------------------------------------------- /test/unit/database/index.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { 3 | buildDataSourceOptions, checkDatabase, 4 | } from '../../../src'; 5 | 6 | describe('src/database/module.ts', () => { 7 | const rootPath : string = path.resolve(process.cwd(), 'test/data/typeorm'); 8 | 9 | it('should build simple connection options', async () => { 10 | const options = await buildDataSourceOptions({ 11 | directory: rootPath, 12 | }); 13 | 14 | expect(options).toBeDefined(); 15 | }); 16 | 17 | it('should check database', async () => { 18 | const options = await buildDataSourceOptions({ 19 | directory: rootPath, 20 | }); 21 | 22 | expect(options).toBeDefined(); 23 | 24 | const check = await checkDatabase({ 25 | options, 26 | }); 27 | 28 | expect(check).toBeDefined(); 29 | expect(check.exists).toBeTruthy(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /docs/guide/index.md: -------------------------------------------------------------------------------- 1 | # What is it? 2 | 3 | This is a library to: 4 | - `create`, `drop` & `seed` the (default-) database 🔥 5 | - manage one or many data-source instances 👻 6 | - parse & apply query parameters (extended **JSON:API** specification & fully typed) to: 7 | - `filter` (related) resources according to one or more criteria, 8 | - reduce (related) resource `fields`, 9 | - `include` related resources, 10 | - `sort` resources according to one or more criteria, 11 | - limit the number of resources returned in a response by `page` limit & offset 12 | 13 | ::: warning **Important NOTE** 14 | 15 | The guide is under construction ☂ at the moment. So please stay patient or contribute to it, till it covers all parts ⭐. 16 | ::: 17 | 18 | ## Limitations 19 | 20 | At the moment, only the following typeorm drivers are supported 21 | to `create` or `drop` a database: 22 | 23 | * CockroachDB 24 | * MSSQL 25 | * MySQL 26 | * Oracle 27 | * Postgres 28 | * SQLite 29 | -------------------------------------------------------------------------------- /src/seeder/module.ts: -------------------------------------------------------------------------------- 1 | import type { DataSource } from 'typeorm'; 2 | import type { SeederEntity } from './entity'; 3 | import { SeederExecutor } from './executor'; 4 | import type { SeederConstructor, SeederOptions } from './type'; 5 | 6 | export async function runSeeder( 7 | dataSource: DataSource, 8 | seeder: SeederConstructor | string, 9 | options: SeederOptions = {}, 10 | ) : Promise { 11 | if (typeof seeder === 'string') { 12 | options.seedName = seeder; 13 | } else { 14 | options.seeds = [seeder]; 15 | } 16 | 17 | const executor = new SeederExecutor(dataSource); 18 | const output = await executor.execute(options); 19 | 20 | return output.pop(); 21 | } 22 | 23 | export async function runSeeders( 24 | dataSource: DataSource, 25 | options?: SeederOptions, 26 | ) : Promise { 27 | const executor = new SeederExecutor(dataSource); 28 | return executor.execute(options); 29 | } 30 | -------------------------------------------------------------------------------- /test/data/typeorm/utils.ts: -------------------------------------------------------------------------------- 1 | import { DataSource } from 'typeorm'; 2 | import { 3 | createDatabase, 4 | dropDatabase, 5 | setDataSource, 6 | unsetDataSource, 7 | } from '../../../src'; 8 | import { createDataSourceOptions } from './factory'; 9 | 10 | export async function setupFsDataSource(name: string) : Promise { 11 | const options = createDataSourceOptions(); 12 | Object.assign(options, { 13 | database: `writable/${name}.sqlite`, 14 | }); 15 | 16 | await createDatabase({ 17 | options, 18 | }); 19 | 20 | const dataSource = new DataSource(options); 21 | await dataSource.initialize(); 22 | 23 | setDataSource(dataSource); 24 | 25 | return dataSource; 26 | } 27 | 28 | export async function destroyTestFsDataSource(dataSource: DataSource) { 29 | await dataSource.destroy(); 30 | 31 | const { options } = dataSource; 32 | 33 | unsetDataSource(); 34 | 35 | await dropDatabase({ options }); 36 | } 37 | -------------------------------------------------------------------------------- /src/helpers/entity/metadata.ts: -------------------------------------------------------------------------------- 1 | import { Repository } from 'typeorm'; 2 | import type { 3 | DataSource, EntityMetadata, EntityTarget, 4 | ObjectLiteral, 5 | } from 'typeorm'; 6 | import { useDataSource } from '../../data-source'; 7 | 8 | /** 9 | * Receive metadata for a given repository or entity-target. 10 | * 11 | * @experimental 12 | * @param input 13 | * @param dataSource 14 | */ 15 | export async function getEntityMetadata( 16 | input: Repository | EntityTarget, 17 | dataSource?: DataSource, 18 | ): Promise { 19 | if (input instanceof Repository) { 20 | return input.metadata; 21 | } 22 | 23 | dataSource = dataSource || await useDataSource(); 24 | 25 | const index = dataSource.entityMetadatas.findIndex( 26 | (entityMetadata) => entityMetadata.target === input, 27 | ); 28 | 29 | if (index === -1) { 30 | throw new Error(`The entity ${input} is not registered.`); 31 | } 32 | 33 | return dataSource.entityMetadatas[index]; 34 | } 35 | -------------------------------------------------------------------------------- /test/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | transform: { 4 | '^.+\\.tsx?$': 'ts-jest', 5 | }, 6 | moduleFileExtensions: [ 7 | 'ts', 8 | 'tsx', 9 | 'js', 10 | 'jsx', 11 | 'json', 12 | 'node', 13 | ], 14 | testRegex: '(/unit/.*|(\\.|/)(test|spec))\\.(ts|js)x?$', 15 | testPathIgnorePatterns: [ 16 | 'dist', 17 | 'unit/mock-util.ts', 18 | ], 19 | coverageDirectory: 'coverage', 20 | collectCoverageFrom: [ 21 | 'src/**/*.{ts,tsx,js,jsx}', 22 | '!src/**/*.d.ts', 23 | '!src/cli/**/*.{ts,js}', 24 | '!src/database/**/*.{ts,js}', 25 | '!src/env/utils.ts', 26 | '!src/errors/*.{ts,js}', 27 | '!src/utils/**/*.{ts,js}', 28 | '!src/seeder/**/*.{ts,js}', 29 | ], 30 | coverageThreshold: { 31 | global: { 32 | branches: 80, 33 | functions: 80, 34 | lines: 80, 35 | statements: 80, 36 | }, 37 | }, 38 | rootDir: '../', 39 | }; 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2024 Peter Placzek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/helpers/entity/property-names.ts: -------------------------------------------------------------------------------- 1 | import type { ObjectLiteral } from 'rapiq'; 2 | import { Repository } from 'typeorm'; 3 | import type { DataSource, EntityMetadata, EntityTarget } from 'typeorm'; 4 | import { getEntityMetadata } from './metadata'; 5 | 6 | /** 7 | * Get (relation-) property names of a given entity. 8 | * 9 | * @experimental 10 | * @param input 11 | * @param dataSource 12 | */ 13 | export async function getEntityPropertyNames( 14 | input: EntityTarget | Repository, 15 | dataSource?: DataSource, 16 | ) : Promise { 17 | let entityMetadata : EntityMetadata; 18 | if (input instanceof Repository) { 19 | entityMetadata = input.metadata; 20 | } else { 21 | entityMetadata = await getEntityMetadata(input, dataSource); 22 | } 23 | 24 | const items : string[] = []; 25 | 26 | for (let i = 0; i < entityMetadata.columns.length; i++) { 27 | items.push(entityMetadata.columns[i].propertyName); 28 | } 29 | 30 | for (let i = 0; i < entityMetadata.relations.length; i++) { 31 | items.push(entityMetadata.relations[i].propertyName); 32 | } 33 | 34 | return items; 35 | } 36 | -------------------------------------------------------------------------------- /test/data/seed/user.ts: -------------------------------------------------------------------------------- 1 | import type { DataSource } from 'typeorm'; 2 | import type { Seeder, SeederFactoryManager } from '../../../src'; 3 | import { User } from '../entity/user'; 4 | 5 | export default class UserSeeder implements Seeder { 6 | track = true; 7 | 8 | public async run( 9 | dataSource: DataSource, 10 | factoryManager: SeederFactoryManager, 11 | ) : Promise { 12 | const repository = dataSource.getRepository(User); 13 | 14 | await repository.insert([ 15 | { 16 | firstName: 'Caleb', lastName: 'Barrows', email: 'caleb.barrows@gmail.com', 17 | }, 18 | ]); 19 | 20 | // --------------------------------------------------- 21 | 22 | const items : User[] = []; 23 | 24 | const userFactory = factoryManager.get(User); 25 | userFactory.setMeta({ foo: 'bar' }); 26 | 27 | // save 1 factory generated entity, to the database 28 | items.push(await userFactory.save()); 29 | 30 | // save 5 factory generated entities, to the database 31 | items.push(...await userFactory.saveMany(5)); 32 | 33 | return items; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/unit/data-source/options/module.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | buildDataSourceOptions, 3 | hasDataSource, 4 | hasDataSourceOptions, 5 | setDataSourceOptions, 6 | unsetDataSource, 7 | useDataSource, 8 | } from '../../../../src'; 9 | import { dataSource } from '../../../data/typeorm/data-source'; 10 | 11 | describe('src/data-source/options', () => { 12 | it('should set and use data-source options', async () => { 13 | setDataSourceOptions(dataSource.options); 14 | 15 | expect(hasDataSourceOptions()).toBeTruthy(); 16 | expect(hasDataSource()).toBeFalsy(); 17 | 18 | const instance = await useDataSource(); 19 | expect(instance.options).toEqual(dataSource.options); 20 | 21 | unsetDataSource(); 22 | }); 23 | 24 | it('should build data-source options', async () => { 25 | const options = await buildDataSourceOptions({ 26 | directory: 'test/data/typeorm', 27 | }); 28 | 29 | expect(options).toBeDefined(); 30 | expect(options.type).toEqual('better-sqlite3'); 31 | expect(options.database).toBeDefined(); 32 | expect(options.extra).toBeDefined(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/seeder/utils/file-path.ts: -------------------------------------------------------------------------------- 1 | /* istanbul ignore next */ 2 | import type { LocatorInfo } from 'locter'; 3 | import { locateMany } from 'locter'; 4 | import path from 'node:path'; 5 | 6 | export async function resolveFilePatterns( 7 | filesPattern: string[], 8 | root?: string, 9 | ) : Promise { 10 | return locateMany( 11 | filesPattern, 12 | { 13 | ...(root ? { path: root } : {}), 14 | ignore: ['**/*.d.ts'], 15 | }, 16 | ).then(buildFilePathname); 17 | } 18 | 19 | export function resolveFilePaths( 20 | filePaths: string[], 21 | root?: string, 22 | ) { 23 | return filePaths.map((filePath) => ( 24 | path.isAbsolute(filePath) ? 25 | filePath : 26 | path.resolve(root || process.cwd(), filePath) 27 | )); 28 | } 29 | 30 | /** 31 | * Exported only for testing purposes 32 | */ 33 | export function buildFilePathname(files: LocatorInfo[]) { 34 | return ( 35 | // sorting by name so that we can define the order of execution using file names 36 | files.sort((a, b) => (a.name > b.name ? 1 : -1)).map((el) => path.join(el.path, el.name + el.extension)) 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /test/unit/helper/entity-uniqueness.spec.ts: -------------------------------------------------------------------------------- 1 | import { isEntityUnique } from '../../../src'; 2 | import { User } from '../../data/entity/user'; 3 | import { createDataSource } from '../../data/typeorm/factory'; 4 | 5 | describe('entity-uniqueness', () => { 6 | it('should check entity uniqueness', async () => { 7 | const dataSource = createDataSource(); 8 | await dataSource.initialize(); 9 | await dataSource.synchronize(); 10 | 11 | const repository = dataSource.getRepository(User); 12 | const user = repository.create({ 13 | firstName: 'foo', 14 | lastName: 'bar', 15 | email: 'foo@gmail.com', 16 | }); 17 | await repository.save(user); 18 | 19 | let isUnique = await isEntityUnique({ 20 | dataSource, 21 | entityTarget: User, 22 | entity: user, 23 | }); 24 | expect(isUnique).toBeFalsy(); 25 | 26 | isUnique = await isEntityUnique({ 27 | dataSource, 28 | entityTarget: User, 29 | entity: user, 30 | entityExisting: user, 31 | }); 32 | expect(isUnique).toBeTruthy(); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/env/type.ts: -------------------------------------------------------------------------------- 1 | import type { DatabaseType } from 'typeorm/driver/types/DatabaseType'; 2 | import type { DataSourceCacheOption } from '../data-source'; 3 | import type { EnvironmentName } from './constants'; 4 | 5 | export interface Environment { 6 | env: `${EnvironmentName}`, 7 | 8 | // Seeder 9 | seeds: string[], 10 | factories: string[], 11 | 12 | // DataSource 13 | type?: DatabaseType, 14 | url?: string, 15 | host?: string, 16 | port?: number, 17 | username?: string, 18 | password?: string, 19 | database?: string, 20 | sid?: string, // string ?? 21 | schema?: string, // string ?? 22 | /** 23 | * dropSchema 24 | */ 25 | schemaDrop?: boolean, 26 | extra?: any, 27 | synchronize?: boolean, 28 | migrations: string[], 29 | migrationsRun?: boolean, 30 | migrationsTableName?: string, 31 | entities: string[], 32 | entityPrefix?: string, 33 | metadataTableName?: string, 34 | subscribers: string[], 35 | logging: string[] | boolean | string, 36 | logger?: string, 37 | maxQueryExecutionTime?: number, 38 | debug?: string, 39 | cache?: DataSourceCacheOption, 40 | uuidExtension?: string 41 | } 42 | -------------------------------------------------------------------------------- /test/data/typeorm/FakeSelectQueryBuilder.ts: -------------------------------------------------------------------------------- 1 | import type { ObjectLiteral } from 'typeorm'; 2 | import { SelectQueryBuilder } from 'typeorm'; 3 | import { dataSource } from './data-source'; 4 | 5 | export class FakeSelectQueryBuilder extends SelectQueryBuilder { 6 | constructor() { 7 | super(dataSource); 8 | } 9 | 10 | override addSelect( 11 | _selection: string | string[] | ((qb: SelectQueryBuilder) => SelectQueryBuilder), 12 | _selectionAliasName?: string, 13 | ): this { 14 | return this; 15 | } 16 | 17 | override leftJoinAndSelect( 18 | // eslint-disable-next-line @typescript-eslint/ban-types 19 | _entityOrProperty: Function | string | ((qb: SelectQueryBuilder) => SelectQueryBuilder), 20 | _alias: string, 21 | // eslint-disable-next-line default-param-last 22 | _condition = '', 23 | _parameters?: ObjectLiteral, 24 | ): this { 25 | return this; 26 | } 27 | 28 | override take(_take?: number): this { 29 | return this; 30 | } 31 | 32 | override skip(_skip?: number): this { 33 | return this; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/unit/seeder/utils/file-path.spec.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import { buildFilePathname } from '../../../../src'; 3 | 4 | describe('src/seeder/utils/file-path.ts', () => { 5 | describe('buildFilePathname', () => { 6 | it('should build file path name', () => { 7 | const files = [ 8 | { 9 | path: '/path/to/dir', 10 | name: '2_file', 11 | extension: '.ts', 12 | }, 13 | { 14 | path: '/path/to/dir', 15 | name: '1_file', 16 | extension: '.ts', 17 | }, 18 | { 19 | path: '/path/to/dir', 20 | name: '0_file', 21 | extension: '.ts', 22 | }, 23 | ]; 24 | const result = buildFilePathname(files); 25 | expect(result).toEqual([ 26 | path.join(path.sep, 'path', 'to', 'dir', '0_file.ts'), 27 | path.join(path.sep, 'path', 'to', 'dir', '1_file.ts'), 28 | path.join(path.sep, 'path', 'to', 'dir', '2_file.ts'), 29 | ]); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/utils/separator.ts: -------------------------------------------------------------------------------- 1 | export function canReplaceWindowsSeparator(input: string) : boolean { 2 | // https://superuser.com/questions/176388/why-does-windows-use-backslashes-for-paths-and-unix-forward-slashes/176395#176395 3 | if (input.startsWith('\\\\?\\')) { 4 | return false; 5 | } 6 | 7 | let characterIndex: number; 8 | 9 | const specialCharacters = ['[', '{', '(', '^', '$', '.', '|', '?', '*', '+']; 10 | for (let i = 0; i < specialCharacters.length; i++) { 11 | characterIndex = input.indexOf(specialCharacters[i]); 12 | if (characterIndex !== -1) { 13 | // special character is prefixed with \, no transformation allowed 14 | if (characterIndex !== 0 && input[characterIndex - 1] === '\\') { 15 | return false; 16 | } 17 | } 18 | } 19 | 20 | return true; 21 | } 22 | 23 | export function replaceWindowSeparator(input: string) { 24 | return input.replace(/\\/g, '/'); 25 | } 26 | 27 | export function safeReplaceWindowsSeparator(input: string): string { 28 | if (input.indexOf('\\') === -1 || !canReplaceWindowsSeparator(input)) { 29 | return input; 30 | } 31 | 32 | return replaceWindowSeparator(input); 33 | } 34 | -------------------------------------------------------------------------------- /test/unit/data-source/options/merge.spec.ts: -------------------------------------------------------------------------------- 1 | import type { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions'; 2 | import { mergeDataSourceOptions } from '../../../../src'; 3 | 4 | describe('src/data-source/options/merge', () => { 5 | it('should merge data source options', () => { 6 | const options = mergeDataSourceOptions({ 7 | type: 'postgres', 8 | password: undefined, 9 | }, { 10 | type: 'postgres', 11 | password: 'password', 12 | }); 13 | 14 | expect(options).toBeDefined(); 15 | expect(options.type).toEqual('postgres'); 16 | expect((options as PostgresConnectionOptions).password).toEqual('password'); 17 | }); 18 | 19 | it('should not merge data source options', () => { 20 | const options = mergeDataSourceOptions({ 21 | type: 'postgres', 22 | password: undefined, 23 | }, { 24 | type: 'mysql', 25 | password: 'password', 26 | }); 27 | 28 | expect(options).toBeDefined(); 29 | expect(options.type).toEqual('postgres'); 30 | expect((options as PostgresConnectionOptions).password).toBeUndefined(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/unit/data-source/singleton.ts: -------------------------------------------------------------------------------- 1 | import { dataSource } from '../../data/typeorm/data-source'; 2 | import { 3 | hasDataSource, 4 | setDataSource, 5 | unsetDataSource, 6 | useDataSource, 7 | } from '../../../src'; 8 | 9 | describe('src/data-source/singleton.ts', () => { 10 | it('should set and use datasource', async () => { 11 | setDataSource(dataSource); 12 | 13 | expect(hasDataSource()).toBeTruthy(); 14 | 15 | let instance = await useDataSource(); 16 | expect(instance).toEqual(dataSource); 17 | 18 | instance = await useDataSource(); 19 | expect(instance).toEqual(dataSource); 20 | 21 | unsetDataSource(); 22 | expect(hasDataSource()).toBeFalsy(); 23 | }); 24 | 25 | it('should set and use data-source with alias', async () => { 26 | expect(hasDataSource('foo')).toBeFalsy(); 27 | 28 | setDataSource(dataSource, 'foo'); 29 | 30 | expect(hasDataSource()).toBeFalsy(); 31 | expect(hasDataSource('foo')).toBeTruthy(); 32 | 33 | const instance = await useDataSource('foo'); 34 | expect(instance).toEqual(dataSource); 35 | 36 | unsetDataSource('foo'); 37 | expect(hasDataSource('foo')).toBeFalsy(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /.github/actions/install/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Install' 2 | description: 'Prepares the repo for a job by checking out and installing dependencies' 3 | inputs: 4 | node-version: 5 | description: 'The node version to setup' 6 | required: true 7 | registry-url: 8 | description: 'Define registry-url' 9 | required: false 10 | 11 | runs: 12 | using: 'composite' 13 | steps: 14 | - name: echo github.ref 15 | shell: bash 16 | run: echo ${{ github.ref }} 17 | 18 | - name: Use Node.js ${{ inputs.node-version }} 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: ${{ inputs.node-version }} 22 | registry-url: ${{ inputs.registry-url }} 23 | 24 | - name: Use cache 25 | uses: actions/cache@v3 26 | with: 27 | path: | 28 | node_modules 29 | */*/node_modules 30 | key: ${{ runner.os }}-install-${{ hashFiles('**/package.json') }} 31 | restore-keys: | 32 | ${{ runner.os }}-install- 33 | 34 | - name: Install 35 | shell: bash 36 | run: | 37 | npm ci 38 | -------------------------------------------------------------------------------- /src/helpers/entity/error.ts: -------------------------------------------------------------------------------- 1 | import { TypeormExtensionError } from '../../errors'; 2 | 3 | type TypeormRelationLookupErrorOptions = { 4 | message: string, 5 | relation: string, 6 | columns: string[] 7 | }; 8 | 9 | export class EntityRelationLookupError extends TypeormExtensionError { 10 | /** 11 | * The property name of the relation. 12 | */ 13 | public relation: string; 14 | 15 | /** 16 | * The property names of the join columns. 17 | */ 18 | public columns: string[]; 19 | 20 | constructor(options: TypeormRelationLookupErrorOptions) { 21 | super(options.message); 22 | 23 | this.relation = options.relation; 24 | this.columns = options.columns; 25 | } 26 | 27 | static notReferenced(relation: string, columns: string[]) { 28 | return new EntityRelationLookupError({ 29 | message: `${relation} entity is not referenced by ${columns.join(', ')}`, 30 | relation, 31 | columns, 32 | }); 33 | } 34 | 35 | static notFound(relation: string, columns: string[]) { 36 | return new EntityRelationLookupError({ 37 | message: `Can't find ${relation} entity by ${columns.join(', ')}`, 38 | relation, 39 | columns, 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/seeder/factory/manager.ts: -------------------------------------------------------------------------------- 1 | import type { EntitySchema, ObjectType } from 'typeorm'; 2 | import type { FactoryCallback, SeederFactoryItem } from './type'; 3 | import { getEntityName, hasOwnProperty } from '../../utils'; 4 | import { SeederFactory } from './module'; 5 | 6 | export class SeederFactoryManager { 7 | public readonly items : Record = {}; 8 | 9 | set, Meta = unknown>( 10 | entity: ObjectType | EntitySchema, 11 | factoryFn: FactoryCallback, 12 | ) : SeederFactoryItem { 13 | const name = getEntityName(entity); 14 | 15 | this.items[name] = { 16 | factoryFn, 17 | entity, 18 | }; 19 | 20 | return this.items[name]; 21 | } 22 | 23 | get, Meta = unknown>( 24 | entity: ObjectType | EntitySchema, 25 | ) : SeederFactory { 26 | const name = getEntityName(entity); 27 | 28 | if (!hasOwnProperty(this.items, name)) { 29 | throw new Error(`No seeder factory is registered for the entity: ${name}`); 30 | } 31 | 32 | return new SeederFactory({ 33 | factoryFn: this.items[name].factoryFn, 34 | entity, 35 | name, 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | target-branch: "master" 7 | schedule: 8 | interval: "daily" 9 | 10 | # Maintain dependencies for npm 11 | - package-ecosystem: "npm" 12 | target-branch: "master" 13 | directory: "/" 14 | schedule: 15 | interval: "daily" 16 | versioning-strategy: "increase" 17 | open-pull-requests-limit: 20 18 | commit-message: 19 | prefix: "fix" 20 | prefix-development: "build" 21 | include: "scope" 22 | groups: 23 | majorProd: 24 | applies-to: version-updates 25 | dependency-type: production 26 | update-types: 27 | - "major" 28 | majorDev: 29 | applies-to: version-updates 30 | dependency-type: development 31 | exclude-patterns: 32 | - "eslint" 33 | - "rapiq" 34 | - "pascal-case" 35 | update-types: 36 | - "major" 37 | minorAndPatch: 38 | applies-to: version-updates 39 | update-types: 40 | - "patch" 41 | - "minor" 42 | -------------------------------------------------------------------------------- /src/data-source/options/singleton.ts: -------------------------------------------------------------------------------- 1 | import type { DataSourceOptions } from 'typeorm'; 2 | import { buildDataSourceOptions } from './module'; 3 | 4 | const instances : Record = {}; 5 | const instancePromises : Record> = {}; 6 | 7 | export function setDataSourceOptions( 8 | options: DataSourceOptions, 9 | alias?: string, 10 | ) { 11 | instances[alias || 'default'] = options; 12 | } 13 | 14 | export function hasDataSourceOptions(alias?: string) : boolean { 15 | return Object.prototype.hasOwnProperty.call(instances, alias || 'default'); 16 | } 17 | 18 | export async function useDataSourceOptions(alias?: string) : Promise { 19 | alias = alias || 'default'; 20 | 21 | if (Object.prototype.hasOwnProperty.call(instances, alias)) { 22 | return instances[alias]; 23 | } 24 | 25 | /* istanbul ignore next */ 26 | if (!Object.prototype.hasOwnProperty.call(instancePromises, alias)) { 27 | instancePromises[alias] = buildDataSourceOptions() 28 | .catch((e) => { 29 | if (alias) { 30 | delete instancePromises[alias]; 31 | } 32 | 33 | throw e; 34 | }); 35 | } 36 | 37 | instances[alias] = await instancePromises[alias]; 38 | 39 | return instances[alias]; 40 | } 41 | -------------------------------------------------------------------------------- /src/database/utils/schema.ts: -------------------------------------------------------------------------------- 1 | import type { DataSourceOptions, Migration } from 'typeorm'; 2 | import { DataSource, InstanceChecker } from 'typeorm'; 3 | 4 | export async function synchronizeDatabaseSchema( 5 | input: DataSource | DataSourceOptions, 6 | ) : Promise { 7 | let dataSource: DataSource; 8 | let options: DataSourceOptions; 9 | 10 | if (InstanceChecker.isDataSource(input)) { 11 | dataSource = input; 12 | options = dataSource.options; 13 | } else { 14 | options = input; 15 | dataSource = new DataSource(options); 16 | } 17 | 18 | if (!dataSource.isInitialized) { 19 | await dataSource.initialize(); 20 | } 21 | 22 | let migrationsCount = 0; 23 | if (options.migrations) { 24 | migrationsCount = Array.isArray(options.migrations) ? 25 | options.migrations.length : 26 | Object.keys(options.migrations).length; 27 | } 28 | 29 | let migrations : Migration[] = []; 30 | 31 | if (migrationsCount > 0) { 32 | migrations = await dataSource.runMigrations({ 33 | transaction: options.migrationsTransactionMode, 34 | }); 35 | } else { 36 | await dataSource.synchronize(false); 37 | } 38 | 39 | if (!InstanceChecker.isDataSource(input)) { 40 | await dataSource.destroy(); 41 | } 42 | 43 | return migrations; 44 | } 45 | -------------------------------------------------------------------------------- /src/database/methods/check/types.ts: -------------------------------------------------------------------------------- 1 | import type { DataSource, DataSourceOptions, Migration } from 'typeorm'; 2 | 3 | export type DatabaseCheckContext = { 4 | /** 5 | * Options for finding the typeorm DataSource. 6 | * 7 | * Default: undefined 8 | */ 9 | options?: DataSourceOptions, 10 | 11 | /** 12 | * Use alias to access already registered DataSource / DataSourceOptions. 13 | * 14 | * default: undefined 15 | */ 16 | alias?: string, 17 | 18 | /** 19 | * Indicates whether to destroy the data-source 20 | * afterward or not. 21 | * If a datasource previously existed, this option will be ignored. 22 | * 23 | * default: true 24 | */ 25 | dataSourceCleanup?: boolean, 26 | 27 | /** 28 | * Use predefined data-source for checks. 29 | * 30 | * default: undefined 31 | */ 32 | dataSource?: DataSource 33 | }; 34 | 35 | export type DatabaseCheckResult = { 36 | /** 37 | * Indicates whether the database 38 | * has already been created or not. 39 | * 40 | * default: false 41 | */ 42 | exists: boolean, 43 | 44 | /** 45 | * Indicates whether the database's schema 46 | * is up-to-date. 47 | * 48 | * default: false 49 | */ 50 | schema: boolean, 51 | 52 | /** 53 | * Array of un applied migrations. 54 | * 55 | * default: [] 56 | */ 57 | migrationsPending: Migration[] 58 | }; 59 | -------------------------------------------------------------------------------- /src/database/methods/type.ts: -------------------------------------------------------------------------------- 1 | import type { DataSourceOptions } from 'typeorm'; 2 | import type { DataSourceFindOptions } from '../../data-source'; 3 | 4 | export type DatabaseBaseContext = { 5 | /** 6 | * Options for finding the typeorm DataSource. 7 | */ 8 | options: DataSourceOptions, 9 | 10 | /** 11 | * Options for the find method, where to look for the data-source file. 12 | */ 13 | findOptions?: DataSourceFindOptions, 14 | 15 | /** 16 | * Initial database to connect. 17 | * 18 | * default: undefined 19 | */ 20 | initialDatabase?: string, 21 | }; 22 | 23 | export type DatabaseBaseContextInput = Partial; 24 | 25 | export type DatabaseCreateContext = Omit & { 26 | /** 27 | * Only create database if not already exist. 28 | * 29 | * default: true 30 | */ 31 | ifNotExist: boolean, 32 | 33 | /** 34 | * Synchronize or migrate the database scheme. 35 | * 36 | * default: true 37 | */ 38 | synchronize: boolean 39 | }; 40 | 41 | export type DatabaseCreateContextInput = Partial; 42 | 43 | export type DatabaseDropContext = Omit & { 44 | 45 | /** 46 | * Only drop database if existed. 47 | * 48 | * Default: true 49 | */ 50 | ifExist?: boolean 51 | }; 52 | 53 | export type DatabaseDropContextInput = Partial; 54 | -------------------------------------------------------------------------------- /src/seeder/type.ts: -------------------------------------------------------------------------------- 1 | import type { DataSource } from 'typeorm'; 2 | import type { TSConfig } from '../utils/tsconfig'; 3 | import type { SeederFactoryItem, SeederFactoryManager } from './factory'; 4 | 5 | export interface Seeder { 6 | /** 7 | * Track seeder execution. 8 | * 9 | * Default: false 10 | */ 11 | track?: boolean; 12 | 13 | run(dataSource: DataSource, factoryManager: SeederFactoryManager) : Promise; 14 | } 15 | 16 | export type SeederConstructor = new () => Seeder; 17 | 18 | export type SeederOptions = { 19 | seeds?: SeederConstructor[] | string[], 20 | seedName?: string, 21 | seedTableName?: string, 22 | seedTracking?: boolean 23 | 24 | factories?: SeederFactoryItem[] | string[], 25 | factoriesLoad?: boolean 26 | }; 27 | 28 | export type SeederExecutorOptions = { 29 | /** 30 | * Root directory of the project. 31 | */ 32 | root?: string, 33 | 34 | /** 35 | * Directory path to the tsconfig.json file 36 | * 37 | * Default: process.cwd() + path.sep + tsconfig.json 38 | */ 39 | tsconfig?: string | TSConfig, 40 | 41 | /** 42 | * This option indicates if file paths should be preserved, 43 | * and treated as if the just-in-time compilation environment is detected. 44 | */ 45 | preserveFilePaths?: boolean, 46 | }; 47 | 48 | export type SeederPrepareElement = { 49 | constructor: SeederConstructor, 50 | timestamp?: number, 51 | fileName?: string, 52 | filePath?: string 53 | }; 54 | -------------------------------------------------------------------------------- /test/unit/seeder/factory.spec.ts: -------------------------------------------------------------------------------- 1 | import type { DataSource } from 'typeorm'; 2 | import { useSeederFactory } from '../../../src'; 3 | import { User } from '../../data/entity/user'; 4 | import { destroyTestFsDataSource, setupFsDataSource } from '../../data/typeorm/utils'; 5 | import '../../data/factory/user'; 6 | 7 | describe('src/seeder/factory/index.ts', () => { 8 | let dataSource : DataSource; 9 | beforeEach(async () => { 10 | dataSource = await setupFsDataSource('factory'); 11 | }); 12 | 13 | afterEach(async () => { 14 | await destroyTestFsDataSource(dataSource); 15 | }); 16 | 17 | it('should create & save seed', async () => { 18 | const user = await useSeederFactory(User).save(); 19 | expect(user).toBeDefined(); 20 | expect(user.id).toBeDefined(); 21 | }); 22 | 23 | it('should create & save many seeds', async () => { 24 | const users = await useSeederFactory(User).saveMany(3); 25 | expect(users).toBeDefined(); 26 | expect(users.length).toEqual(3); 27 | 28 | for (let i = 0; i < users.length; i++) { 29 | expect(users[i].id).toBeDefined(); 30 | } 31 | }); 32 | 33 | it('should create with different locales', async () => { 34 | const factory = useSeederFactory(User); 35 | let user = await factory.save(); 36 | expect(user).toBeDefined(); 37 | 38 | factory.setLocale('de'); 39 | user = await factory.save(); 40 | expect(user).toBeDefined(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /docs/guide/instances.md: -------------------------------------------------------------------------------- 1 | # Instances 2 | 3 | ## Single 4 | 5 | The default DataSource instance can be acquired, by not providing any alias at all or using the key `default`. 6 | If no DataSource instance or DataSourceOptions object is deposited initially, the method will attempt to locate and load 7 | the DataSource file and initialize itself from there. 8 | 9 | ```typescript 10 | import { useDataSource } from 'typeorm-extension'; 11 | 12 | (async () => { 13 | const dataSource : DataSource = await useDataSource(); 14 | })(); 15 | ``` 16 | 17 | Reference(s): 18 | - [useDataSource](datasource-api-reference#usedatasource) 19 | 20 | ## Multiple 21 | 22 | It is also possible to manage multiple DataSource instances. 23 | Therefore, each additional DataSource must be registered under a different alias. 24 | This can be done by either setting the DataSource instance or the DataSourceOptions object for the given alias. 25 | 26 | ```typescript 27 | import { DataSource, DataSourceOptions } from 'typeorm'; 28 | import { setDataSource, useDataSource } from 'typeorm-extension'; 29 | 30 | (async () => { 31 | const secondDataSourceOptions : DataSourceOptions = { 32 | // ... 33 | }; 34 | 35 | const dataSource = new DataSource(secondDataSourceOptions); 36 | setDataSource(dataSource, 'second'); 37 | 38 | const instance : DataSource = await useDataSource('second'); 39 | })(); 40 | ``` 41 | 42 | Reference(s): 43 | - [setDataSource](datasource-api-reference#setdatasource) 44 | - [setDataSourceOptions](datasource-api-reference#setdatasourceoptions) 45 | -------------------------------------------------------------------------------- /src/seeder/entity.ts: -------------------------------------------------------------------------------- 1 | import type { Seeder, SeederConstructor } from './type'; 2 | 3 | export class SeederEntity { 4 | /** 5 | * ID of the seeder. 6 | * 7 | * Indicates order of the executed seeders. 8 | */ 9 | id?: number; 10 | 11 | /** 12 | * Timestamp of the seeder. 13 | */ 14 | timestamp: number; 15 | 16 | /** 17 | * Name of the seeder (class name). 18 | */ 19 | name: string; 20 | 21 | /** 22 | * Instance of seeder constructor. 23 | */ 24 | instance?: Seeder; 25 | 26 | /** 27 | * File name of the seeder. 28 | */ 29 | fileName?: string; 30 | 31 | /** 32 | * File path of the seeder. 33 | */ 34 | filePath?: string; 35 | 36 | /** 37 | * Result of the executed seeder. 38 | */ 39 | result?: unknown; 40 | 41 | constructor(ctx: { 42 | id?: number, 43 | timestamp: number, 44 | name: string, 45 | constructor?: SeederConstructor, 46 | fileName?: string, 47 | filePath?: string 48 | }) { 49 | this.id = ctx.id; 50 | this.timestamp = ctx.timestamp; 51 | this.name = ctx.name; 52 | 53 | if (ctx.constructor) { 54 | this.instance = new ctx.constructor(); 55 | } 56 | 57 | this.fileName = ctx.fileName; 58 | this.filePath = ctx.filePath; 59 | } 60 | 61 | get trackExecution() : boolean | undefined { 62 | if (typeof this.instance === 'undefined') { 63 | return undefined; 64 | } 65 | 66 | return this.instance.track; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /test/unit/helper/entity-property-names.spec.ts: -------------------------------------------------------------------------------- 1 | import { getEntityPropertyNames } from '../../../src'; 2 | import { User } from '../../data/entity/user'; 3 | import { createDataSource } from '../../data/typeorm/factory'; 4 | 5 | describe('entity-property-names', () => { 6 | it('should get entity property names by entity target', async () => { 7 | const dataSource = createDataSource(); 8 | await dataSource.initialize(); 9 | await dataSource.synchronize(); 10 | 11 | const propertyNames = await getEntityPropertyNames(User, dataSource); 12 | expect(propertyNames.length).toBeGreaterThan(0); 13 | expect(propertyNames).toEqual([ 14 | 'id', 15 | 'firstName', 16 | 'lastName', 17 | 'email', 18 | 'roleId', 19 | 'role', 20 | ]); 21 | 22 | await dataSource.destroy(); 23 | }); 24 | 25 | it('should get entity property names by repository', async () => { 26 | const dataSource = createDataSource(); 27 | await dataSource.initialize(); 28 | await dataSource.synchronize(); 29 | 30 | const repository = dataSource.getRepository(User); 31 | const propertyNames = await getEntityPropertyNames(repository, dataSource); 32 | expect(propertyNames.length).toBeGreaterThan(0); 33 | expect(propertyNames).toEqual([ 34 | 'id', 35 | 'firstName', 36 | 'lastName', 37 | 'email', 38 | 'roleId', 39 | 'role', 40 | ]); 41 | 42 | await dataSource.destroy(); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/unit/query/relations.spec.ts: -------------------------------------------------------------------------------- 1 | import type { RelationsParseOutput } from 'rapiq'; 2 | import { FakeSelectQueryBuilder } from '../../data/typeorm/FakeSelectQueryBuilder'; 3 | import { applyQueryRelations, applyRelations } from '../../../src'; 4 | 5 | describe('src/api/includes.ts', () => { 6 | it('should apply request includes', () => { 7 | const queryBuilder = new FakeSelectQueryBuilder(); 8 | 9 | let value = applyQueryRelations(queryBuilder, 'profile', { allowed: ['profile'] }); 10 | expect(value).toEqual([{ key: 'profile', value: 'profile' }] as RelationsParseOutput); 11 | 12 | value = applyRelations(queryBuilder, 'profile', { allowed: ['profile'], defaultAlias: 'user' }); 13 | expect(value).toEqual([{ key: 'user.profile', value: 'profile' }] as RelationsParseOutput); 14 | 15 | value = applyQueryRelations(queryBuilder, ['profile', 'user_roles.role'], { 16 | allowed: ['profile', 'user_roles.role'], 17 | }); 18 | expect(value).toEqual([ 19 | { key: 'user_roles', value: 'user_roles' }, 20 | { key: 'profile', value: 'profile' }, 21 | { key: 'user_roles.role', value: 'role' }, 22 | ] as RelationsParseOutput); 23 | 24 | value = applyQueryRelations(queryBuilder, ['profile', 'user_roles.role'], { 25 | allowed: ['profile', 'user_roles.role'], 26 | defaultAlias: 'user', 27 | }); 28 | expect(value).toEqual([ 29 | { key: 'user.user_roles', value: 'user_roles' }, 30 | { key: 'user.profile', value: 'profile' }, 31 | { key: 'user_roles.role', value: 'role' }, 32 | ] as RelationsParseOutput); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/unit/database/migration.spec.ts: -------------------------------------------------------------------------------- 1 | import type { DataSourceOptions } from 'typeorm'; 2 | import { DataSource } from 'typeorm'; 3 | import { generateMigration } from '../../../src'; 4 | import { Role } from '../../data/entity/role'; 5 | import { User } from '../../data/entity/user'; 6 | 7 | describe('src/database/migration', () => { 8 | it('should generate migration file', async () => { 9 | const options : DataSourceOptions = { 10 | type: 'better-sqlite3', 11 | entities: [User, Role], 12 | database: ':memory:', 13 | extra: { 14 | charset: 'UTF8_GENERAL_CI', 15 | }, 16 | }; 17 | const dataSource = new DataSource(options); 18 | 19 | const output = await generateMigration({ 20 | dataSource, 21 | preview: true, 22 | }); 23 | 24 | expect(output).toBeDefined(); 25 | expect(output.up).toBeDefined(); 26 | expect(output.up.length).toBeGreaterThanOrEqual(1); 27 | expect(output.up[0]).toEqual('await queryRunner.query(`CREATE TABLE "role" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar NOT NULL)`);'); 28 | expect(output.up[1]).toEqual('await queryRunner.query(`CREATE TABLE "user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "firstName" varchar NOT NULL, "lastName" varchar NOT NULL, "email" varchar NOT NULL, "roleId" integer, CONSTRAINT "UQ_c322cd2084cd4b1b2813a900320" UNIQUE ("firstName", "lastName"))`);'); 29 | 30 | expect(output.down).toBeDefined(); 31 | expect(output.down.length).toBeGreaterThanOrEqual(1); 32 | expect(output.down[0]).toEqual('await queryRunner.query(`ALTER TABLE "user" RENAME TO "temporary_user"`);'); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | We welcome contributions from the community! 3 | If you would like to participate in the project, here are some things to consider. 4 | 5 | ## Code of Conduct 6 | Please read and follow the [Code of Conduct](./CODE_OF_CONDUCT.md). 7 | 8 | ## Submission Guidelines 9 | 10 | ### Submitting an Issue 11 | Before starting work on a pull request, please check if there is an open issue that your contribution relates to. 12 | If there is no such issue, please create a new issue to describe your contribution and start the discussion. 13 | 14 | ### Submitting a Pull Request (PR) 15 | When you are ready to submit your contribution, please create a pull request. 16 | Here are some things to consider: 17 | 18 | - Add a descriptive title to the pull request. 19 | - Link the related issue in the pull request. 20 | - Ensure that all tests are successful and no warnings occur. 21 | - Use a descriptive commit message. 22 | 23 | ## Workflow 24 | 25 | - Find an issue that you would like to work on or create a new issue to propose a new feature or improvement. 26 | - Fork the repository on GitHub. 27 | - Create a new branch for your changes. 28 | - Make your changes and commit them to your branch. 29 | - Push your branch to your fork on GitHub. 30 | - Create a new pull request from your branch. 31 | 32 | A maintainer will review your pull request and may ask you to make additional changes 33 | or provide more information before it is merged. 34 | 35 | ## License 36 | By submitting a contribution, you agree to have your contribution published under the project's license. 37 | Please make sure you have the right to submit your contribution under this license. 38 | 39 | ## Acknowledgements 40 | Thank you for wanting to contribute to the project! We appreciate the effort and time you are putting into your contribution. 41 | -------------------------------------------------------------------------------- /src/env/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | oneOf, read, readBool, readInt, toArray, toBool, 3 | } from 'envix'; 4 | import type { DataSourceCacheOption } from '../data-source'; 5 | import { EnvironmentVariableName } from './constants'; 6 | 7 | export function transformLogging(input?: string) : boolean | string | string[] { 8 | const value = toBool(input); 9 | if (typeof value === 'boolean') { 10 | return value; 11 | } 12 | 13 | if (input === 'all') { 14 | return 'all'; 15 | } 16 | 17 | return toArray(input) ?? []; 18 | } 19 | 20 | export function transformCache(input?: string) : DataSourceCacheOption | undefined { 21 | const value = toBool(input); 22 | if (typeof value === 'boolean') { 23 | return value; 24 | } 25 | 26 | if ( 27 | input === 'redis' || 28 | input === 'ioredis' || 29 | input === 'database' || 30 | input === 'ioredis/cluster' 31 | ) { 32 | let options : Record | undefined; 33 | const envCacheOptions = oneOf([ 34 | read(EnvironmentVariableName.CACHE_OPTIONS), 35 | read(EnvironmentVariableName.CACHE_OPTIONS_ALT), 36 | ]); 37 | if (envCacheOptions) { 38 | options = JSON.parse(envCacheOptions); 39 | } 40 | 41 | return { 42 | type: input, 43 | options, 44 | alwaysEnabled: oneOf([ 45 | readBool(EnvironmentVariableName.CACHE_ALWAYS_ENABLED), 46 | readBool(EnvironmentVariableName.CACHE_ALWAYS_ENABLED_ALT), 47 | ]), 48 | duration: oneOf([ 49 | readInt(EnvironmentVariableName.CACHE_DURATION), 50 | readInt(EnvironmentVariableName.CACHE_DURATION_ALT), 51 | ]), 52 | }; 53 | } 54 | 55 | return undefined; 56 | } 57 | -------------------------------------------------------------------------------- /test/unit/env/module.spec.ts: -------------------------------------------------------------------------------- 1 | import { readDataSourceOptionsFromEnv } from '../../../src'; 2 | import { EnvironmentVariableName, resetEnv, useEnv } from '../../../src/env'; 3 | 4 | describe('src/env/**', () => { 5 | it('should reuse env instance', () => { 6 | resetEnv(); 7 | 8 | const env = useEnv(); 9 | expect(env).toBeDefined(); 10 | expect(env).toEqual(useEnv()); 11 | 12 | resetEnv(); 13 | }); 14 | 15 | it('should handle extra env parameter', () => { 16 | resetEnv(); 17 | 18 | const ob : Record = { 19 | foo: 'bar', 20 | }; 21 | process.env = { 22 | ...process.env, 23 | [EnvironmentVariableName.TYPE]: 'better-sqlite3', 24 | [EnvironmentVariableName.DRIVER_EXTRA]: JSON.stringify(ob), 25 | }; 26 | 27 | const options = readDataSourceOptionsFromEnv(); 28 | expect(options).toBeDefined(); 29 | if (options) { 30 | expect(options.extra).toBeDefined(); 31 | expect(options.extra).toEqual(ob); 32 | } 33 | 34 | delete process.env[EnvironmentVariableName.TYPE]; 35 | delete process.env[EnvironmentVariableName.DRIVER_EXTRA]; 36 | 37 | resetEnv(); 38 | }); 39 | 40 | it('should use url to determine type', () => { 41 | resetEnv(); 42 | 43 | process.env = { 44 | ...process.env, 45 | [EnvironmentVariableName.URL]: 'mysql://admin:start123@localhost:3306', 46 | }; 47 | 48 | const options = readDataSourceOptionsFromEnv(); 49 | expect(options).toBeDefined(); 50 | if (options) { 51 | expect(options.type).toEqual('mysql'); 52 | } 53 | 54 | delete process.env[EnvironmentVariableName.URL]; 55 | 56 | resetEnv(); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/database/methods/drop/module.ts: -------------------------------------------------------------------------------- 1 | import { DriverError } from '../../../errors'; 2 | import type { 3 | DatabaseDropContextInput, 4 | } from '../type'; 5 | import { 6 | dropCockroachDBDatabase, 7 | dropMongoDBDatabase, 8 | dropMsSQLDatabase, 9 | dropMySQLDatabase, 10 | dropOracleDatabase, 11 | dropPostgresDatabase, 12 | dropSQLiteDatabase, 13 | } from '../../driver'; 14 | import { buildDatabaseDropContext } from '../../utils/context'; 15 | 16 | /** 17 | * Drop database for specified driver in ConnectionOptions. 18 | * 19 | * @throws DriverError 20 | * @throws OptionsError 21 | * 22 | * @param input 23 | */ 24 | export async function dropDatabase(input: DatabaseDropContextInput = {}) : Promise { 25 | const context = await buildDatabaseDropContext(input); 26 | 27 | let output : unknown | undefined; 28 | 29 | switch (context.options.type) { 30 | case 'mongodb': 31 | output = await dropMongoDBDatabase(context); 32 | break; 33 | case 'mysql': 34 | case 'mariadb': 35 | output = await dropMySQLDatabase(context); 36 | break; 37 | case 'postgres': 38 | output = await dropPostgresDatabase(context); 39 | break; 40 | case 'cockroachdb': 41 | output = await dropCockroachDBDatabase(context); 42 | break; 43 | case 'sqlite': 44 | case 'better-sqlite3': 45 | output = await dropSQLiteDatabase(context); 46 | break; 47 | case 'oracle': 48 | output = await dropOracleDatabase(context); 49 | break; 50 | case 'mssql': 51 | output = await dropMsSQLDatabase(context); 52 | break; 53 | default: 54 | throw DriverError.notSupported(context.options.type); 55 | } 56 | 57 | return output; 58 | } 59 | -------------------------------------------------------------------------------- /src/database/utils/context.ts: -------------------------------------------------------------------------------- 1 | import type { DataSourceOptions } from 'typeorm'; 2 | import { buildDataSourceOptions, findDataSource } from '../../data-source'; 3 | import type { 4 | DatabaseBaseContextInput, 5 | DatabaseCreateContext, 6 | DatabaseCreateContextInput, 7 | DatabaseDropContext, 8 | DatabaseDropContextInput, 9 | } from '../methods'; 10 | 11 | async function normalizeDataSourceOptions(context: DatabaseBaseContextInput) : Promise { 12 | let options : DataSourceOptions | undefined; 13 | if (context.options) { 14 | options = { 15 | ...context.options, 16 | }; 17 | } else { 18 | const dataSource = await findDataSource(context.findOptions); 19 | if (dataSource) { 20 | options = { 21 | ...dataSource.options, 22 | }; 23 | } 24 | 25 | if (!options) { 26 | options = { 27 | ...await buildDataSourceOptions(), 28 | }; 29 | } 30 | } 31 | 32 | return { 33 | ...options, 34 | 35 | subscribers: [], 36 | synchronize: false, 37 | migrationsRun: false, 38 | dropSchema: false, 39 | }; 40 | } 41 | 42 | export async function buildDatabaseCreateContext( 43 | input: DatabaseCreateContextInput = {}, 44 | ) : Promise { 45 | return { 46 | ...input, 47 | options: await normalizeDataSourceOptions(input), 48 | synchronize: input.synchronize ?? true, 49 | ifNotExist: input.ifNotExist ?? true, 50 | }; 51 | } 52 | 53 | export async function buildDatabaseDropContext( 54 | input: DatabaseDropContextInput = {}, 55 | ) : Promise { 56 | return { 57 | ...input, 58 | options: await normalizeDataSourceOptions(input), 59 | ifExist: input.ifExist ?? true, 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /src/database/methods/create/module.ts: -------------------------------------------------------------------------------- 1 | import { DriverError } from '../../../errors'; 2 | import type { 3 | DatabaseCreateContextInput, 4 | } from '../type'; 5 | import { 6 | createCockroachDBDatabase, 7 | createMongoDBDatabase, 8 | createMsSQLDatabase, 9 | createMySQLDatabase, 10 | createOracleDatabase, 11 | createPostgresDatabase, 12 | createSQLiteDatabase, 13 | } from '../../driver'; 14 | import { buildDatabaseCreateContext } from '../../utils/context'; 15 | 16 | /** 17 | * Create database for specified driver in ConnectionOptions. 18 | * 19 | * @throws DriverError 20 | * @throws OptionsError 21 | * 22 | * @param input 23 | */ 24 | export async function createDatabase(input: DatabaseCreateContextInput = {}) : Promise { 25 | const context = await buildDatabaseCreateContext(input); 26 | 27 | let output : unknown | undefined; 28 | 29 | switch (context.options.type) { 30 | case 'mongodb': 31 | output = await createMongoDBDatabase(context); 32 | break; 33 | case 'mysql': 34 | case 'mariadb': 35 | output = await createMySQLDatabase(context); 36 | break; 37 | case 'postgres': 38 | output = await createPostgresDatabase(context); 39 | break; 40 | case 'cockroachdb': 41 | output = await createCockroachDBDatabase(context); 42 | break; 43 | case 'sqlite': 44 | case 'better-sqlite3': 45 | output = await createSQLiteDatabase(context); 46 | break; 47 | case 'oracle': 48 | output = await createOracleDatabase(context); 49 | break; 50 | case 'mssql': 51 | output = await createMsSQLDatabase(context); 52 | break; 53 | default: 54 | throw DriverError.notSupported(context.options.type); 55 | } 56 | 57 | return output; 58 | } 59 | -------------------------------------------------------------------------------- /src/query/parameter/pagination/module.ts: -------------------------------------------------------------------------------- 1 | import type { ObjectLiteral } from 'rapiq'; 2 | import { parseQueryPagination } from 'rapiq'; 3 | import type { SelectQueryBuilder } from 'typeorm'; 4 | import type { QueryPaginationApplyOptions, QueryPaginationApplyOutput } from './type'; 5 | 6 | /** 7 | * Apply parsed page/pagination parameter data on the db query. 8 | * 9 | * @param query 10 | * @param data 11 | */ 12 | export function applyQueryPaginationParseOutput( 13 | query: SelectQueryBuilder, 14 | data: QueryPaginationApplyOutput, 15 | ) { 16 | /* istanbul ignore next */ 17 | if (typeof data.limit !== 'undefined') { 18 | query.take(data.limit); 19 | 20 | if (typeof data.offset === 'undefined') { 21 | query.skip(0); 22 | } 23 | } 24 | 25 | /* istanbul ignore next */ 26 | if (typeof data.offset !== 'undefined') { 27 | query.skip(data.offset); 28 | } 29 | 30 | return data; 31 | } 32 | 33 | /** 34 | * Apply raw page/pagination parameter data on the db query. 35 | * 36 | * @param query 37 | * @param data 38 | * @param options 39 | */ 40 | export function applyQueryPagination( 41 | query: SelectQueryBuilder, 42 | data: unknown, 43 | options?: QueryPaginationApplyOptions, 44 | ) : QueryPaginationApplyOutput { 45 | return applyQueryPaginationParseOutput(query, parseQueryPagination(data, options)); 46 | } 47 | 48 | /** 49 | * Apply raw page/pagination parameter data on the db query. 50 | * 51 | * @param query 52 | * @param data 53 | * @param options 54 | */ 55 | export function applyPagination( 56 | query: SelectQueryBuilder, 57 | data: unknown, 58 | options?: QueryPaginationApplyOptions, 59 | ) : QueryPaginationApplyOutput { 60 | return applyQueryPagination(query, data, options); 61 | } 62 | -------------------------------------------------------------------------------- /src/database/driver/sqlite.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import fs from 'node:fs'; 3 | import { OptionsError } from '../../errors'; 4 | import type { 5 | DatabaseCreateContextInput, 6 | DatabaseDropContextInput, 7 | } from '../methods'; 8 | import { buildDriverOptions } from './utils'; 9 | import { buildDatabaseCreateContext, buildDatabaseDropContext, synchronizeDatabaseSchema } from '../utils'; 10 | 11 | export async function createSQLiteDatabase( 12 | input: DatabaseCreateContextInput = {}, 13 | ) : Promise { 14 | const context = await buildDatabaseCreateContext(input); 15 | const options = buildDriverOptions(context.options); 16 | if (!options.database) { 17 | throw OptionsError.databaseNotDefined(); 18 | } 19 | 20 | const filePath : string = path.isAbsolute(options.database) ? 21 | options.database : 22 | path.join(process.cwd(), options.database); 23 | 24 | const directoryPath : string = path.dirname(filePath); 25 | 26 | await fs.promises.access(directoryPath, fs.constants.W_OK); 27 | 28 | if (context.synchronize) { 29 | await synchronizeDatabaseSchema(context.options); 30 | } 31 | } 32 | 33 | export async function dropSQLiteDatabase( 34 | input: DatabaseDropContextInput = {}, 35 | ) { 36 | const context = await buildDatabaseDropContext(input); 37 | const options = buildDriverOptions(context.options); 38 | if (!options.database) { 39 | throw OptionsError.databaseNotDefined(); 40 | } 41 | 42 | const filePath : string = path.isAbsolute(options.database) ? 43 | options.database : 44 | path.join(process.cwd(), options.database); 45 | 46 | try { 47 | await fs.promises.access(filePath, fs.constants.F_OK | fs.constants.W_OK); 48 | if (context.ifExist) { 49 | await fs.promises.unlink(filePath); 50 | } 51 | } catch (e) { 52 | // ... 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/unit/query/index.spec.ts: -------------------------------------------------------------------------------- 1 | import type { ParseOutput } from 'rapiq'; 2 | import type { QueryFieldsApplyOutput } from '../../../src'; 3 | import { applyQuery, applyQueryParseOutput } from '../../../src'; 4 | import { FakeSelectQueryBuilder } from '../../data/typeorm/FakeSelectQueryBuilder'; 5 | 6 | describe('src/api/sort.ts', () => { 7 | const query = new FakeSelectQueryBuilder(); 8 | 9 | it('should apply query', () => { 10 | const data = applyQuery( 11 | query, 12 | { 13 | fields: ['id', 'name', 'fake'], 14 | }, 15 | { 16 | defaultAlias: 'user', 17 | fields: { 18 | allowed: ['id', 'name'], 19 | }, 20 | }, 21 | ); 22 | 23 | expect(data.fields).toEqual([ 24 | { key: 'id', path: 'user' }, 25 | { key: 'name', path: 'user' }, 26 | ] as QueryFieldsApplyOutput); 27 | }); 28 | 29 | it('should apply query parse output', () => { 30 | let data = applyQueryParseOutput(query, { 31 | relations: [], 32 | fields: [], 33 | filters: [], 34 | pagination: {}, 35 | sort: [], 36 | }); 37 | expect(data).toEqual({ 38 | relations: [], 39 | fields: [], 40 | filters: [], 41 | pagination: {}, 42 | sort: [], 43 | } as ParseOutput); 44 | 45 | data = applyQueryParseOutput(query, { 46 | defaultPath: 'user', 47 | relations: [], 48 | fields: [], 49 | filters: [], 50 | pagination: {}, 51 | sort: [], 52 | }); 53 | expect(data).toEqual({ 54 | defaultPath: 'user', 55 | relations: [], 56 | fields: [], 57 | filters: [], 58 | pagination: {}, 59 | sort: [], 60 | } as ParseOutput); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /src/query/parameter/sort/module.ts: -------------------------------------------------------------------------------- 1 | import type { SortDirection, SortParseOutput } from 'rapiq'; 2 | import { parseQuerySort } from 'rapiq'; 3 | import type { ObjectLiteral, SelectQueryBuilder } from 'typeorm'; 4 | import { buildKeyWithPrefix } from '../../utils'; 5 | import type { QuerySortApplyOptions, QuerySortApplyOutput } from './type'; 6 | 7 | // -------------------------------------------------- 8 | 9 | /** 10 | * Apply parsed sort parameter data on the db query. 11 | * 12 | * @param query 13 | * @param data 14 | */ 15 | export function applyQuerySortParseOutput( 16 | query: SelectQueryBuilder, 17 | data: SortParseOutput, 18 | ) : QuerySortApplyOutput { 19 | if (data.length === 0) { 20 | return data; 21 | } 22 | 23 | const sort : Record = {}; 24 | 25 | for (let i = 0; i < data.length; i++) { 26 | const key = buildKeyWithPrefix(data[i].key, data[i].path); 27 | 28 | sort[key] = data[i].value; 29 | } 30 | 31 | query.orderBy(sort); 32 | 33 | return data; 34 | } 35 | 36 | /** 37 | * Apply raw sort parameter data on the db query. 38 | * 39 | * @param query 40 | * @param data 41 | * @param options 42 | */ 43 | export function applyQuerySort( 44 | query: SelectQueryBuilder, 45 | data: unknown, 46 | options?: QuerySortApplyOptions, 47 | ) : SortParseOutput { 48 | options = options || {}; 49 | if (options.defaultAlias) { 50 | options.defaultPath = options.defaultAlias; 51 | } 52 | 53 | return applyQuerySortParseOutput(query, parseQuerySort(data, options)); 54 | } 55 | 56 | /** 57 | * Apply raw sort parameter data on the db query. 58 | * 59 | * @param query 60 | * @param data 61 | * @param options 62 | */ 63 | export function applySort( 64 | query: SelectQueryBuilder, 65 | data: unknown, 66 | options?: QuerySortApplyOptions, 67 | ) : SortParseOutput { 68 | return applyQuerySort(query, data, options); 69 | } 70 | -------------------------------------------------------------------------------- /src/query/parameter/fields/module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | parseQueryFields, 3 | } from 'rapiq'; 4 | 5 | import type { ObjectLiteral, SelectQueryBuilder } from 'typeorm'; 6 | import { buildKeyWithPrefix, getAliasForPath } from '../../utils'; 7 | import type { QueryFieldsApplyOptions, QueryFieldsApplyOutput } from './type'; 8 | 9 | /** 10 | * Apply parsed fields parameter data on the db query. 11 | * 12 | * @param query 13 | * @param data 14 | */ 15 | /* istanbul ignore next */ 16 | export function applyQueryFieldsParseOutput( 17 | query: SelectQueryBuilder, 18 | data: QueryFieldsApplyOutput, 19 | options: QueryFieldsApplyOptions = {}, 20 | ) { 21 | if (data.length === 0) { 22 | return data; 23 | } 24 | 25 | query.select(data.map((field) => { 26 | const alias = getAliasForPath(options.relations, field.path) || 27 | options.defaultAlias || 28 | options.defaultPath; 29 | 30 | return buildKeyWithPrefix(field.key, alias); 31 | })); 32 | 33 | return data; 34 | } 35 | 36 | /** 37 | * Apply raw fields parameter data on the db query. 38 | * 39 | * @param query 40 | * @param data 41 | * @param options 42 | */ 43 | export function applyQueryFields( 44 | query: SelectQueryBuilder, 45 | data: unknown, 46 | options?: QueryFieldsApplyOptions, 47 | ) : QueryFieldsApplyOutput { 48 | options = options || {}; 49 | if (options.defaultAlias) { 50 | options.defaultPath = options.defaultAlias; 51 | } 52 | 53 | return applyQueryFieldsParseOutput(query, parseQueryFields(data, options), options); 54 | } 55 | 56 | /** 57 | * Apply raw fields parameter data on the db query. 58 | * 59 | * @param query 60 | * @param data 61 | * @param options 62 | */ 63 | export function applyFields( 64 | query: SelectQueryBuilder, 65 | data: unknown, 66 | options?: QueryFieldsApplyOptions, 67 | ) : QueryFieldsApplyOutput { 68 | return applyQueryFields(query, data, options); 69 | } 70 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | The goal is to create a community that is open and welcoming to all individuals. 3 | To achieve this, we have developed a code of conduct that outlines the expectations for behavior of all members of our community. 4 | 5 | ## Pledge 6 | This community is founded on respect and understanding. 7 | All members are expected to treat others with respect and empathy, and to not tolerate any form of discrimination, 8 | harassment, or attacks. 9 | 10 | ## Expectations 11 | Examples of behavior that contributes to creating a positive environment include: 12 | 13 | - Using welcoming and inclusive language 14 | - Being respectful of differing viewpoints and experiences 15 | - Gracefully accepting constructive criticism 16 | - Focusing on what is best for the community 17 | - Showing empathy towards other community members 18 | 19 | Examples of unacceptable behavior by participants include: 20 | 21 | - The use of sexualized language or imagery and sexual attention or advances 22 | - Trolling, insulting/derogatory comments, and personal or political attacks 23 | - Public or private harassment 24 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 25 | - Other conduct which could reasonably be considered inappropriate in a professional setting 26 | 27 | ## Responsibilities 28 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate 29 | and fair corrective action in response to any instances of unacceptable behavior. 30 | 31 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, 32 | issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily 33 | or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 34 | 35 | ## Contact 36 | If you feel uncomfortable or believe that someone has violated the code of conduct, p 37 | lease contact us at [admin@tada5hi.net](mailto:admin@tada5hi.net). 38 | We will thoroughly investigate the incident and aim for the best possible outcome. 39 | -------------------------------------------------------------------------------- /src/database/driver/mongodb.ts: -------------------------------------------------------------------------------- 1 | import type { MongoDriver } from 'typeorm/driver/mongodb/MongoDriver'; 2 | import type { 3 | DatabaseCreateContextInput, 4 | DatabaseDropContextInput, 5 | } from '../methods'; 6 | import type { DriverOptions } from './types'; 7 | import { buildDriverOptions, createDriver } from './utils'; 8 | import { buildDatabaseCreateContext, buildDatabaseDropContext, synchronizeDatabaseSchema } from '../utils'; 9 | 10 | export async function createSimpleMongoDBConnection( 11 | driver: MongoDriver, 12 | options: DriverOptions, 13 | ) { 14 | /** 15 | * mongodb library 16 | */ 17 | const { MongoClient } = driver.mongodb; 18 | 19 | let url = 'mongodb://'; 20 | if (options.user && options.password) { 21 | url += `${options.user}:${options.password}@`; 22 | } 23 | 24 | url += `${options.host || '127.0.0.1'}:${options.port || 27017}/${options.database}`; 25 | if (options.ssl) { 26 | url += '?tls=true'; 27 | } 28 | 29 | const client = new MongoClient(url); 30 | await client.connect(); 31 | return client; 32 | } 33 | 34 | export async function createMongoDBDatabase( 35 | input: DatabaseCreateContextInput = {}, 36 | ) { 37 | const context = await buildDatabaseCreateContext(input); 38 | const options = buildDriverOptions(context.options); 39 | const driver = createDriver(context.options) as MongoDriver; 40 | 41 | // connection setup, will create the database on the fly. 42 | const client = await createSimpleMongoDBConnection(driver, options); 43 | await client.close(); 44 | 45 | if (context.synchronize) { 46 | await synchronizeDatabaseSchema(context.options); 47 | } 48 | } 49 | 50 | export async function dropMongoDBDatabase( 51 | input: DatabaseDropContextInput = {}, 52 | ) { 53 | const context = await buildDatabaseDropContext(input); 54 | const options = buildDriverOptions(context.options); 55 | const driver = createDriver(context.options) as MongoDriver; 56 | 57 | const client = await createSimpleMongoDBConnection(driver, options); 58 | const result = await client.dropDatabase(); 59 | await client.close(); 60 | 61 | return result; 62 | } 63 | -------------------------------------------------------------------------------- /src/data-source/options/module.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from 'locter'; 2 | import type { DataSourceOptions } from 'typeorm'; 3 | import { OptionsError } from '../../errors'; 4 | import { adjustFilePaths, readTSConfig } from '../../utils'; 5 | import type { TSConfig } from '../../utils'; 6 | import { findDataSource } from '../find'; 7 | import type { DataSourceOptionsBuildContext } from './type'; 8 | import { 9 | mergeDataSourceOptionsWithEnv, 10 | readDataSourceOptionsFromEnv, 11 | } from './utils'; 12 | 13 | /** 14 | * Build DataSourceOptions from DataSource or from configuration. 15 | * 16 | * @param context 17 | */ 18 | export async function buildDataSourceOptions( 19 | context: DataSourceOptionsBuildContext = {}, 20 | ) : Promise { 21 | const directory : string = context.directory || process.cwd(); 22 | 23 | let tsconfig : TSConfig | undefined; 24 | if (!context.preserveFilePaths) { 25 | if (isObject(context.tsconfig)) { 26 | tsconfig = context.tsconfig; 27 | } else { 28 | tsconfig = await readTSConfig(context.tsconfig); 29 | } 30 | } 31 | 32 | const dataSource = await findDataSource({ 33 | directory, 34 | fileName: context.dataSourceName, 35 | tsconfig, 36 | }); 37 | 38 | if (dataSource) { 39 | if (context.preserveFilePaths) { 40 | return mergeDataSourceOptionsWithEnv(dataSource.options); 41 | } 42 | 43 | const options = await adjustFilePaths( 44 | dataSource.options, 45 | [ 46 | 'entities', 47 | 'migrations', 48 | 'subscribers', 49 | ], 50 | tsconfig, 51 | ); 52 | 53 | return mergeDataSourceOptionsWithEnv(options); 54 | } 55 | 56 | const options = readDataSourceOptionsFromEnv(); 57 | if (options) { 58 | if (context.preserveFilePaths) { 59 | return options; 60 | } 61 | 62 | return adjustFilePaths( 63 | options, 64 | ['entities', 'migrations', 'subscribers'], 65 | tsconfig, 66 | ); 67 | } 68 | 69 | throw OptionsError.notFound(); 70 | } 71 | -------------------------------------------------------------------------------- /src/query/parameter/relations/module.ts: -------------------------------------------------------------------------------- 1 | import type { RelationsParseOutput } from 'rapiq'; 2 | import { parseQueryRelations } from 'rapiq'; 3 | import type { ObjectLiteral, SelectQueryBuilder } from 'typeorm'; 4 | import { buildKeyWithPrefix } from '../../utils'; 5 | import type { QueryRelationsApplyOptions, QueryRelationsApplyOutput } from './type'; 6 | 7 | /** 8 | * Apply parsed include/relation parameter data on the db query. 9 | * 10 | * @param query 11 | * @param data 12 | * @param options 13 | */ 14 | export function applyQueryRelationsParseOutput( 15 | query: SelectQueryBuilder, 16 | data: RelationsParseOutput, 17 | options?: QueryRelationsApplyOptions, 18 | ) : QueryRelationsApplyOutput { 19 | options = options || {}; 20 | for (let i = 0; i < data.length; i++) { 21 | const parts = data[i].key.split('.'); 22 | 23 | let key : string; 24 | if (parts.length > 1) { 25 | key = parts.slice(-2).join('.'); 26 | } else { 27 | key = buildKeyWithPrefix(data[i].key, options.defaultAlias); 28 | } 29 | 30 | data[i].key = key; 31 | 32 | /* istanbul ignore next */ 33 | query.leftJoinAndSelect(key, data[i].value); 34 | } 35 | 36 | return data; 37 | } 38 | 39 | /** 40 | * Apply raw include/relations parameter data on the db query. 41 | * 42 | * @param query 43 | * @param data 44 | * @param options 45 | */ 46 | export function applyQueryRelations( 47 | query: SelectQueryBuilder, 48 | data: unknown, 49 | options?: QueryRelationsApplyOptions, 50 | ) : QueryRelationsApplyOutput { 51 | return applyQueryRelationsParseOutput(query, parseQueryRelations(data, options), options); 52 | } 53 | 54 | /** 55 | * Apply raw include/relations parameter data on the db query. 56 | * 57 | * @param query 58 | * @param data 59 | * @param options 60 | */ 61 | export function applyRelations( 62 | query: SelectQueryBuilder, 63 | data: unknown, 64 | options?: QueryRelationsApplyOptions, 65 | ) : QueryRelationsApplyOutput { 66 | return applyQueryRelations(query, data, options); 67 | } 68 | -------------------------------------------------------------------------------- /src/database/driver/utils/build.ts: -------------------------------------------------------------------------------- 1 | import type { DataSourceOptions } from 'typeorm'; 2 | import { DriverUtils } from 'typeorm/driver/DriverUtils'; 3 | import { getCharsetFromDataSourceOptions } from './charset'; 4 | import { getCharacterSetFromDataSourceOptions } from './character-set'; 5 | import type { DriverOptions } from '../types'; 6 | 7 | export function buildDriverOptions(options: DataSourceOptions): DriverOptions { 8 | let driverOptions: Record; 9 | 10 | switch (options.type) { 11 | case 'mysql': 12 | case 'mariadb': 13 | case 'postgres': 14 | case 'cockroachdb': 15 | case 'mssql': 16 | case 'oracle': 17 | driverOptions = DriverUtils.buildDriverOptions(options.replication ? options.replication.master : options); 18 | break; 19 | case 'mongodb': 20 | driverOptions = DriverUtils.buildMongoDBDriverOptions(options); 21 | break; 22 | default: 23 | driverOptions = DriverUtils.buildDriverOptions(options); 24 | } 25 | 26 | const charset = getCharsetFromDataSourceOptions(options); 27 | const characterSet = getCharacterSetFromDataSourceOptions(options); 28 | 29 | return { 30 | host: driverOptions.host, 31 | user: driverOptions.user || driverOptions.username, 32 | password: driverOptions.password, 33 | database: driverOptions.database, 34 | port: driverOptions.port, 35 | ...(charset ? { charset } : {}), 36 | ...(characterSet ? { characterSet } : {}), 37 | ...(driverOptions.ssl ? { ssl: driverOptions.ssl } : {}), 38 | ...(driverOptions.url ? { url: driverOptions.url } : {}), 39 | ...(driverOptions.connectString ? { connectString: driverOptions.connectString } : {}), 40 | ...(driverOptions.sid ? { sid: driverOptions.sid } : {}), 41 | ...(driverOptions.serviceName ? { serviceName: driverOptions.serviceName } : {}), 42 | ...(driverOptions.template ? { template: driverOptions.template } : {}), 43 | ...(options.extra ? { extra: options.extra } : {}), 44 | ...(driverOptions.domain ? { domain: driverOptions.domain } : {}), 45 | ...(driverOptions.schema ? { schema: driverOptions.schema } : {}), 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/seeder/factory/utils.ts: -------------------------------------------------------------------------------- 1 | import { load } from 'locter'; 2 | import type { EntitySchema, ObjectType } from 'typeorm'; 3 | import { resolveFilePaths, resolveFilePatterns } from '../utils'; 4 | import { SeederFactoryManager } from './manager'; 5 | import type { FactoryCallback, SeederFactoryItem } from './type'; 6 | 7 | let instance : SeederFactoryManager | undefined; 8 | 9 | export function useSeederFactoryManager() { 10 | if (typeof instance !== 'undefined') { 11 | return instance; 12 | } 13 | 14 | instance = new SeederFactoryManager(); 15 | 16 | return instance; 17 | } 18 | 19 | export function setSeederFactory, Meta = unknown>( 20 | entity: ObjectType | EntitySchema, 21 | factoryFn: FactoryCallback, 22 | ) : SeederFactoryItem { 23 | const manager = useSeederFactoryManager(); 24 | return manager.set(entity, factoryFn); 25 | } 26 | 27 | export function useSeederFactory>( 28 | entity: ObjectType | EntitySchema, 29 | ) { 30 | const manager = useSeederFactoryManager(); 31 | return manager.get(entity); 32 | } 33 | 34 | export async function prepareSeederFactories( 35 | items: SeederFactoryItem[] | string[], 36 | root?: string, 37 | ) { 38 | let factoryFiles: string[] = []; 39 | const factoryConfigs: SeederFactoryItem[] = []; 40 | 41 | for (let i = 0; i < items.length; i++) { 42 | const value = items[i]; 43 | if (typeof value === 'string') { 44 | factoryFiles.push(value); 45 | } else { 46 | factoryConfigs.push(value); 47 | } 48 | } 49 | 50 | if (factoryFiles.length > 0) { 51 | factoryFiles = await resolveFilePatterns(factoryFiles, root); 52 | factoryFiles = resolveFilePaths(factoryFiles, root); 53 | 54 | for (let i = 0; i < factoryFiles.length; i++) { 55 | await load(factoryFiles[i]); 56 | } 57 | } 58 | 59 | if (factoryConfigs.length > 0) { 60 | const factoryManager = useSeederFactoryManager(); 61 | 62 | for (let i = 0; i < factoryConfigs.length; i++) { 63 | factoryManager.set( 64 | factoryConfigs[i].entity, 65 | factoryConfigs[i].factoryFn, 66 | ); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/unit/data-source/find.spec.ts: -------------------------------------------------------------------------------- 1 | import { InstanceChecker } from 'typeorm'; 2 | import path from 'path'; 3 | import { findDataSource } from '../../../src'; 4 | 5 | describe('src/data-source/utils/find.ts', () => { 6 | it('should find and load data-source', async () => { 7 | let dataSource = await findDataSource({ 8 | directory: path.join(__dirname, '..', '..', 'data', 'typeorm'), 9 | }); 10 | 11 | expect(dataSource).toBeDefined(); 12 | expect(InstanceChecker.isDataSource(dataSource)); 13 | if (dataSource) { 14 | expect(dataSource.options.extra).toBeDefined(); 15 | } 16 | 17 | dataSource = await findDataSource({ 18 | directory: 'test/data/typeorm', 19 | }); 20 | 21 | expect(dataSource).toBeDefined(); 22 | expect(InstanceChecker.isDataSource(dataSource)); 23 | }); 24 | 25 | it('should find and load async data-source', async () => { 26 | const dataSource = await findDataSource({ 27 | directory: path.join(__dirname, '..', '..', 'data', 'typeorm'), 28 | fileName: 'data-source-async', 29 | }); 30 | 31 | expect(dataSource).toBeDefined(); 32 | expect(InstanceChecker.isDataSource(dataSource)); 33 | }); 34 | 35 | it('should find data-source with windows separator', async () => { 36 | const dataSource = await findDataSource({ 37 | directory: 'test\\data\\typeorm', 38 | }); 39 | 40 | expect(dataSource).toBeDefined(); 41 | expect(InstanceChecker.isDataSource(dataSource)); 42 | }); 43 | 44 | it('should find data-source with default export', async () => { 45 | let dataSource = await findDataSource({ 46 | fileName: 'data-source-default', 47 | directory: path.join(__dirname, '..', '..', 'data', 'typeorm'), 48 | }); 49 | 50 | expect(dataSource).toBeDefined(); 51 | expect(InstanceChecker.isDataSource(dataSource)); 52 | if (dataSource) { 53 | expect(dataSource.options.extra).toBeUndefined(); 54 | } 55 | 56 | dataSource = await findDataSource({ 57 | fileName: 'data-source-default', 58 | directory: 'test/data/typeorm', 59 | }); 60 | 61 | expect(dataSource).toBeDefined(); 62 | expect(InstanceChecker.isDataSource(dataSource)); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/unit/seeder/seeder.spec.ts: -------------------------------------------------------------------------------- 1 | import type { DataSource } from 'typeorm'; 2 | import { 3 | SeederExecutor, 4 | runSeeder, 5 | runSeeders, 6 | } from '../../../src'; 7 | import type { SeederEntity } from '../../../src'; 8 | import { User } from '../../data/entity/user'; 9 | import { destroyTestFsDataSource, setupFsDataSource } from '../../data/typeorm/utils'; 10 | import '../../data/factory/user'; 11 | import UserSeeder from '../../data/seed/user'; 12 | 13 | describe('src/seeder/index.ts', () => { 14 | let dataSource : DataSource; 15 | 16 | beforeEach(async () => { 17 | dataSource = await setupFsDataSource('seeder'); 18 | }); 19 | 20 | afterEach(async () => { 21 | await destroyTestFsDataSource(dataSource); 22 | }); 23 | 24 | it('should seed with data-source options', async () => { 25 | const executor = new SeederExecutor(dataSource); 26 | let output = await executor.execute(); 27 | expect(output.length).toEqual(2); 28 | 29 | output = await executor.execute(); 30 | expect(output.length).toEqual(0); 31 | 32 | const repository = dataSource.getRepository(User); 33 | const entities = await repository.find(); 34 | 35 | expect(entities).toBeDefined(); 36 | expect(entities.length).toBeGreaterThanOrEqual(7); 37 | }); 38 | 39 | it('should seed with explicit definitions', async () => { 40 | await runSeeders(dataSource, { 41 | seeds: [UserSeeder], 42 | }); 43 | 44 | const repository = dataSource.getRepository(User); 45 | const entities = await repository.find(); 46 | 47 | expect(entities).toBeDefined(); 48 | expect(entities.length).toBeGreaterThanOrEqual(7); 49 | }); 50 | 51 | it('should seed with explicit definition', async () => { 52 | const response = await runSeeder(dataSource, UserSeeder); 53 | expect(response).toBeDefined(); 54 | 55 | const { result } = (response as SeederEntity); 56 | expect(Array.isArray(result)).toBeTruthy(); 57 | if (Array.isArray(result)) { 58 | expect(result.length).toEqual(6); 59 | } 60 | 61 | const repository = dataSource.getRepository(User); 62 | const entities = await repository.find(); 63 | 64 | expect(entities).toBeDefined(); 65 | expect(entities.length).toBeGreaterThanOrEqual(7); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /docs/.vitepress/config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress'; 2 | 3 | export default defineConfig({ 4 | title: 'typeorm-extension', 5 | base: '/', 6 | themeConfig: { 7 | socialLinks: [ 8 | { icon: 'github', link: 'https://github.com/tada5hi/typeorm-extension' }, 9 | ], 10 | editLink: { 11 | pattern: 'https://github.com/tada5hi/typeorm-extension/edit/master/docs/:path', 12 | text: 'Edit this page on GitHub' 13 | }, 14 | nav: [ 15 | { 16 | text: 'Home', 17 | link: '/', 18 | activeMatch: '/', 19 | }, 20 | { 21 | text: 'Guide', 22 | link: '/guide/', 23 | activeMatch: '/guide/', 24 | } 25 | ], 26 | sidebar: { 27 | '/guide/': [ 28 | { 29 | text: 'Introduction', 30 | items: [ 31 | {text: 'What is it?', link: '/guide/'}, 32 | ] 33 | }, 34 | { 35 | text: 'Getting Started', 36 | items: [ 37 | {text: 'Installation', link: '/guide/installation'}, 38 | {text: 'CLI', link: '/guide/cli'}, 39 | {text: 'Database', link: '/guide/database'}, 40 | {text: 'Instances', link: '/guide/instances'}, 41 | {text: 'Seeding', link: '/guide/seeding'}, 42 | {text: 'Query', link: '/guide/query'}, 43 | ] 44 | }, 45 | { 46 | text: 'API Reference', 47 | items: [ 48 | {text: 'Database', link: '/guide/database-api-reference'}, 49 | {text: 'DataSource', link: '/guide/datasource-api-reference'}, 50 | {text: 'Seeding', link: '/guide/seeding-api-reference'}, 51 | {text: 'Query', link: '/guide/query-api-reference'}, 52 | ] 53 | }, 54 | { 55 | text: 'Upgrading', 56 | items: [ 57 | {text: 'Version 3', link: '/guide/migration-guide-v3' } 58 | ] 59 | } 60 | ] 61 | } 62 | } 63 | }); 64 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: CI 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | pull_request: 11 | types: 12 | - opened 13 | - synchronize 14 | 15 | env: 16 | PRIMARY_NODE_VERSION: 22 17 | 18 | jobs: 19 | install: 20 | name: Checkout and Install 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v6 25 | - name: Install 26 | uses: ./.github/actions/install 27 | with: 28 | node-version: ${{ env.PRIMARY_NODE_VERSION }} 29 | 30 | lint: 31 | name: Lint 32 | needs: [build] 33 | runs-on: ubuntu-latest 34 | steps: 35 | - name: Checkout 36 | uses: actions/checkout@v6 37 | 38 | - name: Install 39 | uses: ./.github/actions/install 40 | with: 41 | node-version: ${{ env.PRIMARY_NODE_VERSION }} 42 | 43 | - name: Build 44 | uses: ./.github/actions/build 45 | 46 | - name: Lint 47 | run: | 48 | npm run lint 49 | 50 | build: 51 | name: Build 52 | needs: [install] 53 | runs-on: ubuntu-latest 54 | steps: 55 | - name: Checkout 56 | uses: actions/checkout@v6 57 | - name: Install 58 | uses: ./.github/actions/install 59 | with: 60 | node-version: ${{ env.PRIMARY_NODE_VERSION }} 61 | - name: Build 62 | uses: ./.github/actions/build 63 | 64 | tests: 65 | name: Test 66 | needs: [build] 67 | runs-on: ubuntu-latest 68 | steps: 69 | - name: Checkout 70 | uses: actions/checkout@v6 71 | 72 | - name: Install 73 | uses: ./.github/actions/install 74 | with: 75 | node-version: ${{ env.PRIMARY_NODE_VERSION }} 76 | 77 | - name: Build 78 | uses: ./.github/actions/build 79 | 80 | - name: Run tests 81 | run: | 82 | npm run test 83 | -------------------------------------------------------------------------------- /src/seeder/utils/prepare.ts: -------------------------------------------------------------------------------- 1 | import { load } from 'locter'; 2 | import path from 'node:path'; 3 | import type { SeederConstructor, SeederPrepareElement } from '../type'; 4 | import { resolveFilePaths, resolveFilePatterns } from './file-path'; 5 | 6 | export async function prepareSeederSeeds( 7 | input: SeederConstructor[] | string[], 8 | root?: string, 9 | ): Promise { 10 | const items: SeederPrepareElement[] = []; 11 | 12 | let seedFiles: string[] = []; 13 | const seedConstructors: SeederConstructor[] = []; 14 | 15 | for (let i = 0; i < input.length; i++) { 16 | const value = input[i]; 17 | if (typeof value === 'string') { 18 | seedFiles.push(value); 19 | } else { 20 | seedConstructors.push(value); 21 | } 22 | } 23 | 24 | if (seedFiles.length > 0) { 25 | seedFiles = await resolveFilePatterns(seedFiles, root); 26 | seedFiles = resolveFilePaths(seedFiles, root); 27 | 28 | for (let i = 0; i < seedFiles.length; i++) { 29 | const moduleExports = await load(seedFiles[i]); 30 | 31 | let clazzConstructor : SeederConstructor | undefined; 32 | 33 | const exportKeys = Object.keys(moduleExports); 34 | for (let j = 0; j < exportKeys.length; j++) { 35 | const moduleExport = moduleExports[exportKeys[j]]; 36 | if ( 37 | typeof moduleExport === 'function' && 38 | moduleExport.prototype 39 | ) { 40 | clazzConstructor = moduleExport; 41 | } 42 | } 43 | 44 | if (clazzConstructor) { 45 | const fileName = path.basename(seedFiles[i]); 46 | const filePath = seedFiles[i]; 47 | const match = fileName.match(/^([0-9]{13,})-(.*)$/); 48 | 49 | let timestamp : number | undefined; 50 | if (match) { 51 | timestamp = parseInt(match[1], 10); 52 | } 53 | 54 | items.push({ 55 | constructor: clazzConstructor, 56 | fileName, 57 | filePath, 58 | ...(timestamp ? { timestamp } : {}), 59 | }); 60 | } 61 | } 62 | } 63 | 64 | if (seedConstructors.length > 0) { 65 | for (let i = 0; i < seedConstructors.length; i++) { 66 | items.push({ 67 | constructor: seedConstructors[i], 68 | }); 69 | } 70 | } 71 | 72 | return items; 73 | } 74 | -------------------------------------------------------------------------------- /src/database/driver/oracle.ts: -------------------------------------------------------------------------------- 1 | import type { OracleDriver } from 'typeorm/driver/oracle/OracleDriver'; 2 | import type { 3 | DatabaseCreateContextInput, 4 | DatabaseDropContextInput, 5 | } from '../methods'; 6 | import type { DriverOptions } from './types'; 7 | import { buildDriverOptions, createDriver } from './utils'; 8 | import { buildDatabaseCreateContext, synchronizeDatabaseSchema } from '../utils'; 9 | 10 | export function createSimpleOracleConnection( 11 | driver: OracleDriver, 12 | options: DriverOptions, 13 | ) { 14 | const { getConnection } = driver.oracle; 15 | 16 | if (!options.connectString) { 17 | let address = '(PROTOCOL=TCP)'; 18 | 19 | if (options.host) { 20 | address += `(HOST=${options.host})`; 21 | } 22 | 23 | if (options.port) { 24 | address += `(PORT=${options.port})`; 25 | } 26 | 27 | let connectData = '(SERVER=DEDICATED)'; 28 | 29 | if (options.sid) { 30 | connectData += `(SID=${options.sid})`; 31 | } 32 | 33 | if (options.serviceName) { 34 | connectData += `(SERVICE_NAME=${options.serviceName})`; 35 | } 36 | 37 | options.connectString = `(DESCRIPTION=(ADDRESS=${address})(CONNECT_DATA=${connectData}))`; 38 | } 39 | 40 | return getConnection({ 41 | user: options.user, 42 | password: options.password, 43 | connectString: options.connectString || options.url, 44 | ...(options.extra ? options.extra : {}), 45 | }); 46 | } 47 | 48 | export async function createOracleDatabase( 49 | input: DatabaseCreateContextInput = {}, 50 | ) { 51 | const context = await buildDatabaseCreateContext(input); 52 | const options = buildDriverOptions(context.options); 53 | const driver = createDriver(context.options) as OracleDriver; 54 | 55 | const connection = createSimpleOracleConnection(driver, options); 56 | /** 57 | * @link https://github.com/typeorm/typeorm/blob/master/src/driver/oracle/OracleQueryRunner.ts#L295 58 | */ 59 | const query = `CREATE DATABASE IF NOT EXISTS ${options.database}`; 60 | 61 | const result = await connection.execute(query); 62 | 63 | if (context.synchronize) { 64 | await synchronizeDatabaseSchema(context.options); 65 | } 66 | 67 | return result; 68 | } 69 | 70 | export async function dropOracleDatabase( 71 | _context: DatabaseDropContextInput = {}, 72 | ) { 73 | /** 74 | * @link https://github.com/typeorm/typeorm/blob/master/src/driver/oracle/OracleQueryRunner.ts#L295 75 | */ 76 | 77 | return Promise.resolve(); 78 | } 79 | -------------------------------------------------------------------------------- /src/database/driver/cockroachdb.ts: -------------------------------------------------------------------------------- 1 | import type { CockroachDriver } from 'typeorm/driver/cockroachdb/CockroachDriver'; 2 | import type { DatabaseCreateContextInput, DatabaseDropContextInput } from '../methods'; 3 | import { createSimplePostgresConnection } from './postgres'; 4 | import { buildDriverOptions, createDriver } from './utils'; 5 | import { buildDatabaseCreateContext, buildDatabaseDropContext, synchronizeDatabaseSchema } from '../utils'; 6 | 7 | export async function executeSimpleCockroachDBQuery(connection: any, query: string, endConnection = true) { 8 | return new Promise(((resolve, reject) => { 9 | connection.query(query, (queryErr: any, queryResult: any) => { 10 | if (endConnection) { 11 | connection.end(); 12 | } 13 | 14 | if (queryErr) { 15 | reject(queryErr); 16 | } 17 | 18 | resolve(queryResult); 19 | }); 20 | })); 21 | } 22 | 23 | export async function createCockroachDBDatabase( 24 | input: DatabaseCreateContextInput = {}, 25 | ) { 26 | const context = await buildDatabaseCreateContext(input); 27 | const options = buildDriverOptions(context.options); 28 | const driver = createDriver(context.options) as CockroachDriver; 29 | 30 | const connection = await createSimplePostgresConnection( 31 | driver, 32 | options, 33 | context, 34 | ); 35 | 36 | /** 37 | * @link https://github.com/typeorm/typeorm/blob/master/src/driver/cockroachdb/CockroachQueryRunner.ts#L347 38 | */ 39 | const query = `CREATE DATABASE ${context.ifNotExist ? 'IF NOT EXISTS ' : ''} "${options.database}"`; 40 | const result = await executeSimpleCockroachDBQuery(connection, query); 41 | 42 | if (context.synchronize) { 43 | await synchronizeDatabaseSchema(context.options); 44 | } 45 | 46 | return result; 47 | } 48 | 49 | export async function dropCockroachDBDatabase( 50 | input: DatabaseDropContextInput = {}, 51 | ) { 52 | const context = await buildDatabaseDropContext(input); 53 | const options = buildDriverOptions(context.options); 54 | const driver = createDriver(context.options) as CockroachDriver; 55 | 56 | const connection = await createSimplePostgresConnection( 57 | driver, 58 | options, 59 | context, 60 | ); 61 | /** 62 | * @link https://github.com/typeorm/typeorm/blob/master/src/driver/cockroachdb/CockroachQueryRunner.ts#L356 63 | */ 64 | const query = `DROP DATABASE ${context.ifExist ? 'IF EXISTS ' : ''} "${options.database}"`; 65 | 66 | return executeSimpleCockroachDBQuery(connection, query); 67 | } 68 | -------------------------------------------------------------------------------- /src/database/driver/mssql.ts: -------------------------------------------------------------------------------- 1 | import type { SqlServerDriver } from 'typeorm/driver/sqlserver/SqlServerDriver'; 2 | import type { 3 | DatabaseCreateContextInput, 4 | DatabaseDropContextInput, 5 | } from '../methods'; 6 | import type { DriverOptions } from './types'; 7 | import { buildDriverOptions, createDriver } from './utils'; 8 | import { buildDatabaseCreateContext, buildDatabaseDropContext, synchronizeDatabaseSchema } from '../utils'; 9 | 10 | export async function createSimpleMsSQLConnection( 11 | driver: SqlServerDriver, 12 | options: DriverOptions, 13 | ) { 14 | const option : Record = { 15 | user: options.user, 16 | password: options.password, 17 | server: options.host, 18 | port: options.port || 1433, 19 | ...(options.extra ? options.extra : {}), 20 | ...(options.domain ? { domain: options.domain } : {}), 21 | }; 22 | 23 | await driver.mssql.connect(option); 24 | 25 | return driver.mssql; 26 | } 27 | 28 | export async function createMsSQLDatabase( 29 | input: DatabaseCreateContextInput = {}, 30 | ) { 31 | const context = await buildDatabaseCreateContext(input); 32 | const options = buildDriverOptions(context.options); 33 | const driver = createDriver(context.options) as SqlServerDriver; 34 | 35 | const connection = await createSimpleMsSQLConnection(driver, options); 36 | /** 37 | * @link https://github.com/typeorm/typeorm/blob/master/src/driver/sqlserver/SqlServerQueryRunner.ts#L416 38 | */ 39 | let query = context.ifNotExist ? 40 | `IF DB_ID('${options.database}') IS NULL CREATE DATABASE "${options.database}"` : 41 | `CREATE DATABASE "${options.database}"`; 42 | 43 | if (typeof options.characterSet === 'string') { 44 | query += ` CHARACTER SET ${options.characterSet}`; 45 | } 46 | 47 | const result = await connection.query(query); 48 | 49 | if (context.synchronize) { 50 | await synchronizeDatabaseSchema(context.options); 51 | } 52 | 53 | return result; 54 | } 55 | 56 | export async function dropMsSQLDatabase( 57 | input: DatabaseDropContextInput = {}, 58 | ) { 59 | const context = await buildDatabaseDropContext(input); 60 | const options = buildDriverOptions(context.options); 61 | const driver = createDriver(context.options) as SqlServerDriver; 62 | 63 | const connection = await createSimpleMsSQLConnection(driver, options); 64 | /** 65 | * @link https://github.com/typeorm/typeorm/blob/master/src/driver/sqlserver/SqlServerQueryRunner.ts#L425 66 | */ 67 | const query = context.ifExist ? 68 | `IF DB_ID('${options.database}') IS NOT NULL DROP DATABASE "${options.database}"` : 69 | `DROP DATABASE "${options.database}"`; 70 | 71 | return connection.query(query); 72 | } 73 | -------------------------------------------------------------------------------- /test/unit/data-source/options/env.spec.ts: -------------------------------------------------------------------------------- 1 | import type { DataSourceOptions } from 'typeorm'; 2 | import { 3 | EnvironmentVariableName, 4 | buildDataSourceOptions, 5 | hasEnvDataSourceOptions, 6 | mergeDataSourceOptionsWithEnv, 7 | readDataSourceOptionsFromEnv, 8 | resetEnv, 9 | } from '../../../../src'; 10 | import { User } from '../../../data/entity/user'; 11 | 12 | describe('src/data-source/options/env', () => { 13 | it('should read env data-source options', () => { 14 | process.env[ 15 | EnvironmentVariableName.URL 16 | ] = 'mysql://admin:start123@localhost:3306'; 17 | 18 | expect(hasEnvDataSourceOptions()).toEqual(true); 19 | 20 | const env = readDataSourceOptionsFromEnv(); 21 | expect(env).toBeDefined(); 22 | if (env) { 23 | expect(env.type).toEqual('mysql'); 24 | if (env.type === 'mysql') { 25 | expect(env.url).toEqual('mysql://admin:start123@localhost:3306'); 26 | } else { 27 | expect(true).toEqual(false); 28 | } 29 | } 30 | 31 | delete process.env[EnvironmentVariableName.URL]; 32 | 33 | resetEnv(); 34 | }); 35 | 36 | it('should merge data-source options', () => { 37 | let options : DataSourceOptions = { 38 | type: 'better-sqlite3', 39 | entities: [User], 40 | database: ':memory:', 41 | extra: { 42 | charset: 'UTF8_GENERAL_CI', 43 | }, 44 | }; 45 | 46 | process.env = { 47 | ...process.env, 48 | [EnvironmentVariableName.TYPE]: 'better-sqlite3', 49 | [EnvironmentVariableName.DATABASE]: 'db.sqlite', 50 | }; 51 | 52 | options = mergeDataSourceOptionsWithEnv(options); 53 | 54 | expect(options.type).toEqual('better-sqlite3'); 55 | expect(options.database).toEqual('db.sqlite'); 56 | 57 | resetEnv(); 58 | }); 59 | 60 | it('should build data-source options with experimental option', async () => { 61 | process.env = { 62 | ...process.env, 63 | [EnvironmentVariableName.TYPE]: 'better-sqlite3', 64 | [EnvironmentVariableName.DATABASE]: 'test.sqlite', 65 | }; 66 | 67 | const options = await buildDataSourceOptions({ 68 | directory: 'test/data/typeorm', 69 | }); 70 | 71 | expect(options).toBeDefined(); 72 | expect(options.type).toEqual('better-sqlite3'); 73 | expect(options.database).toEqual('test.sqlite'); 74 | 75 | delete process.env[EnvironmentVariableName.TYPE]; 76 | delete process.env[EnvironmentVariableName.DATABASE]; 77 | 78 | resetEnv(); 79 | }); 80 | 81 | it('should not read from env', () => { 82 | const options = readDataSourceOptionsFromEnv(); 83 | expect(options).toBeUndefined(); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /test/unit/helper/entity-join-columns.spec.ts: -------------------------------------------------------------------------------- 1 | import { EntityRelationLookupError, validateEntityJoinColumns } from '../../../src'; 2 | import { Role } from '../../data/entity/role'; 3 | import { User } from '../../data/entity/user'; 4 | import { createDataSource } from '../../data/typeorm/factory'; 5 | 6 | describe('entity-relation-columns', () => { 7 | it('should validate entity relation columns', async () => { 8 | const dataSource = createDataSource(); 9 | await dataSource.initialize(); 10 | await dataSource.synchronize(); 11 | 12 | const roleRepository = dataSource.getRepository(Role); 13 | const role = roleRepository.create({ 14 | name: 'foo', 15 | }); 16 | 17 | await roleRepository.save(role); 18 | 19 | const userRepository = dataSource.getRepository(User); 20 | const user = userRepository.create({ 21 | firstName: 'foo', 22 | lastName: 'bar', 23 | email: 'foo@gmail.com', 24 | roleId: role.id, 25 | }); 26 | 27 | await validateEntityJoinColumns(user, { dataSource, entityTarget: User }); 28 | 29 | expect(user.role).toBeDefined(); 30 | 31 | await dataSource.destroy(); 32 | }); 33 | 34 | it('should validate entity nullable relation columns', async () => { 35 | const dataSource = createDataSource(); 36 | await dataSource.initialize(); 37 | await dataSource.synchronize(); 38 | 39 | const userRepository = dataSource.getRepository(User); 40 | const user = userRepository.create({ 41 | firstName: 'foo', 42 | lastName: 'bar', 43 | email: 'foo@gmail.com', 44 | roleId: null, 45 | }); 46 | 47 | await validateEntityJoinColumns(user, { dataSource, entityTarget: User }); 48 | 49 | expect(user.roleId).toBeFalsy(); 50 | expect(user.role).toBeUndefined(); 51 | }); 52 | 53 | it('should not validate entity relation columns', async () => { 54 | const dataSource = createDataSource(); 55 | await dataSource.initialize(); 56 | await dataSource.synchronize(); 57 | 58 | const userRepository = dataSource.getRepository(User); 59 | const user = userRepository.create({ 60 | firstName: 'foo', 61 | lastName: 'bar', 62 | email: 'foo@gmail.com', 63 | roleId: 1000, 64 | }); 65 | 66 | expect.assertions(3); 67 | 68 | try { 69 | await validateEntityJoinColumns(user, { dataSource, entityTarget: User }); 70 | } catch (e) { 71 | expect(e).toBeDefined(); 72 | 73 | if (e instanceof EntityRelationLookupError) { 74 | expect(e.relation).toEqual('role'); 75 | expect(e.columns).toEqual(['roleId']); 76 | } 77 | } 78 | 79 | await dataSource.destroy(); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /src/helpers/entity/join-columns.ts: -------------------------------------------------------------------------------- 1 | import type { ObjectLiteral } from 'rapiq'; 2 | import type { DataSource, EntityTarget, FindOptionsWhere } from 'typeorm'; 3 | import { useDataSource } from '../../data-source'; 4 | import { EntityRelationLookupError } from './error'; 5 | import { getEntityMetadata } from './metadata'; 6 | 7 | type EntityRelationColumnsValidateOptions = { 8 | dataSource?: DataSource, 9 | entityTarget: EntityTarget, 10 | }; 11 | 12 | /** 13 | * Validate join columns of a given entity. 14 | * It will look up and append the referenced entities to the input entity. 15 | * 16 | * @experimental 17 | * @param entity 18 | * @param options 19 | */ 20 | export async function validateEntityJoinColumns( 21 | entity: Partial, 22 | options: EntityRelationColumnsValidateOptions, 23 | ) { 24 | const dataSource = options.dataSource || await useDataSource(); 25 | const entityMetadata = await getEntityMetadata(options.entityTarget, dataSource); 26 | 27 | const relations : Partial = {}; 28 | for (let i = 0; i < entityMetadata.relations.length; i++) { 29 | const relation = entityMetadata.relations[i]; 30 | 31 | let skipRelation : boolean = false; 32 | 33 | const where : FindOptionsWhere = {}; 34 | const columns : string[] = []; 35 | for (let j = 0; j < relation.joinColumns.length; j++) { 36 | const joinColumn = relation.joinColumns[j]; 37 | if (typeof entity[joinColumn.propertyName] === 'undefined') { 38 | continue; 39 | } 40 | 41 | if ( 42 | joinColumn.isNullable && 43 | entity[joinColumn.propertyName] === null 44 | ) { 45 | skipRelation = true; 46 | break; 47 | } 48 | 49 | if (joinColumn.referencedColumn) { 50 | where[joinColumn.referencedColumn.propertyName] = entity[joinColumn.propertyName]; 51 | 52 | columns.push(joinColumn.propertyName); 53 | } else { 54 | throw EntityRelationLookupError.notReferenced( 55 | relation.propertyName, 56 | [joinColumn.propertyName], 57 | ); 58 | } 59 | } 60 | 61 | if (skipRelation || columns.length === 0) { 62 | continue; 63 | } 64 | 65 | const repository = dataSource.getRepository(relation.type); 66 | const item = await repository.findOne({ 67 | where, 68 | }); 69 | 70 | if (!item) { 71 | throw EntityRelationLookupError.notFound(relation.propertyName, columns); 72 | } 73 | 74 | relations[relation.propertyName as keyof T] = item as T[keyof T]; 75 | } 76 | 77 | const relationKeys = Object.keys(relations); 78 | for (let i = 0; i < relationKeys.length; i++) { 79 | const relationKey = relationKeys[i]; 80 | 81 | entity[relationKey as keyof T] = relations[relationKey] as T[keyof T]; 82 | } 83 | 84 | return entity; 85 | } 86 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | env: 11 | NODE_VERSION: 22 12 | 13 | permissions: 14 | issues: write 15 | packages: write 16 | contents: write 17 | pull-requests: write 18 | 19 | defaults: 20 | run: 21 | shell: bash 22 | 23 | jobs: 24 | release: 25 | name: Release 26 | runs-on: ubuntu-latest 27 | 28 | steps: 29 | - uses: google-github-actions/release-please-action@v4 30 | id: release 31 | with: 32 | token: ${{ secrets.GITHUB_TOKEN }} 33 | 34 | - name: Checkout 35 | if: steps.release.outputs.releases_created == 'true' 36 | uses: actions/checkout@v6 37 | with: 38 | fetch-depth: 0 39 | 40 | - name: Configure Git 41 | if: steps.release.outputs.releases_created == 'true' 42 | run: | 43 | git config user.name "$GITHUB_ACTOR" 44 | git config user.email "$GITHUB_ACTOR@users.noreply.github.com" 45 | 46 | - name: Install 47 | if: steps.release.outputs.releases_created == 'true' 48 | uses: ./.github/actions/install 49 | with: 50 | node-version: ${{ env.NODE_VERSION }} 51 | 52 | - name: Build 53 | if: steps.release.outputs.releases_created == 'true' 54 | uses: ./.github/actions/build 55 | 56 | - name: Publish 57 | if: steps.release.outputs.releases_created == 'true' 58 | run: npx workspaces-publish 59 | env: 60 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 61 | 62 | - name: Coverage 63 | if: steps.release.outputs.releases_created == 'true' 64 | run: | 65 | npm run test:coverage 66 | 67 | - name: Upload report 68 | if: steps.release.outputs.releases_created == 'true' 69 | uses: codecov/codecov-action@v5.5.1 70 | with: 71 | token: ${{ secrets.codecov }} 72 | directory: ./coverage/ 73 | 74 | - name: Build docs package 75 | if: steps.release.outputs.releases_created == 'true' 76 | run: npm run docs:build 77 | 78 | - name: Set docs CNAME 79 | if: steps.release.outputs.releases_created == 'true' 80 | run: | 81 | cd ./docs/.vitepress/dist/ 82 | touch CNAME 83 | echo "typeorm-extension.tada5hi.net" > CNAME 84 | 85 | - name: Deploy docs 86 | if: steps.release.outputs.releases_created == 'true' 87 | uses: peaceiris/actions-gh-pages@v4 88 | with: 89 | github_token: ${{ secrets.GITHUB_TOKEN }} 90 | publish_dir: ./docs/.vitepress/dist 91 | 92 | 93 | -------------------------------------------------------------------------------- /src/query/module.ts: -------------------------------------------------------------------------------- 1 | import type { ParseInput, ParseOutput } from 'rapiq'; 2 | import { parseQuery } from 'rapiq'; 3 | import type { ObjectLiteral, SelectQueryBuilder } from 'typeorm'; 4 | import { 5 | applyQueryFieldsParseOutput, 6 | applyQueryFiltersParseOutput, 7 | applyQueryPaginationParseOutput, 8 | applyQueryRelationsParseOutput, 9 | applyQuerySortParseOutput, 10 | } from './parameter'; 11 | import type { QueryApplyOptions, QueryApplyOutput } from './type'; 12 | import { isQueryOptionDefined } from './utils'; 13 | 14 | export function applyQueryParseOutput( 15 | query: SelectQueryBuilder, 16 | context: ParseOutput, 17 | ): ParseOutput { 18 | if (context.fields) { 19 | applyQueryFieldsParseOutput(query, context.fields, { 20 | defaultAlias: context.defaultPath, 21 | relations: context.relations, 22 | }); 23 | } 24 | 25 | if (context.filters) { 26 | applyQueryFiltersParseOutput(query, context.filters, { 27 | defaultAlias: context.defaultPath, 28 | relations: context.relations, 29 | }); 30 | } 31 | 32 | if (context.pagination) { 33 | applyQueryPaginationParseOutput(query, context.pagination); 34 | } 35 | 36 | if (context.relations) { 37 | applyQueryRelationsParseOutput(query, context.relations, { 38 | defaultAlias: context.defaultPath, 39 | }); 40 | } 41 | 42 | if (context.sort) { 43 | applyQuerySortParseOutput(query, context.sort); 44 | } 45 | 46 | return context; 47 | } 48 | 49 | export function applyQuery( 50 | query: SelectQueryBuilder, 51 | input: ParseInput, 52 | options?: QueryApplyOptions, 53 | ) : QueryApplyOutput { 54 | options = options || {}; 55 | 56 | if (options.defaultAlias) { 57 | options.defaultPath = options.defaultAlias; 58 | } 59 | 60 | if ( 61 | typeof options.fields === 'undefined' || 62 | !isQueryOptionDefined(options.fields, ['allowed', 'default']) 63 | ) { 64 | options.fields = false; 65 | } 66 | 67 | if ( 68 | typeof options.filters === 'undefined' || 69 | !isQueryOptionDefined(options.filters, ['allowed', 'default']) 70 | ) { 71 | options.filters = false; 72 | } 73 | 74 | if ( 75 | typeof options.pagination === 'undefined' 76 | ) { 77 | options.pagination = false; 78 | } 79 | 80 | if ( 81 | typeof options.relations === 'undefined' || 82 | !isQueryOptionDefined(options.relations, ['allowed']) 83 | ) { 84 | options.relations = false; 85 | } 86 | 87 | if ( 88 | typeof options.sort === 'undefined' || 89 | !isQueryOptionDefined(options.sort, ['allowed', 'default']) 90 | ) { 91 | options.sort = false; 92 | } 93 | 94 | const output = applyQueryParseOutput(query, parseQuery(input, options)); 95 | 96 | return { 97 | ...output, 98 | ...(options.defaultAlias ? { defaultAlias: options.defaultAlias } : {}), 99 | }; 100 | } 101 | -------------------------------------------------------------------------------- /src/data-source/singleton.ts: -------------------------------------------------------------------------------- 1 | import type { DataSourceOptions } from 'typeorm'; 2 | import { DataSource } from 'typeorm'; 3 | import { useDataSourceOptions } from './options'; 4 | 5 | const instances : Record = {}; 6 | 7 | const initializePromises : Record> = {}; 8 | const optionsPromises: Record> = {}; 9 | 10 | export function setDataSource( 11 | dataSource: DataSource, 12 | alias?: string, 13 | ) { 14 | alias = alias || 'default'; 15 | 16 | instances[alias] = dataSource; 17 | } 18 | 19 | export function hasDataSource(alias?: string) : boolean { 20 | alias = alias || 'default'; 21 | 22 | return Object.prototype.hasOwnProperty.call(instances, alias); 23 | } 24 | 25 | export function unsetDataSource(alias?: string) { 26 | alias = alias || 'default'; 27 | 28 | if (Object.prototype.hasOwnProperty.call(instances, alias)) { 29 | delete instances[alias]; 30 | } 31 | 32 | /* istanbul ignore next */ 33 | if (Object.prototype.hasOwnProperty.call(optionsPromises, alias)) { 34 | delete optionsPromises[alias]; 35 | } 36 | 37 | /* istanbul ignore next */ 38 | if (Object.prototype.hasOwnProperty.call(initializePromises, alias)) { 39 | delete initializePromises[alias]; 40 | } 41 | } 42 | 43 | export async function useDataSource(alias?: string) : Promise { 44 | alias = alias || 'default'; 45 | 46 | if (Object.prototype.hasOwnProperty.call(instances, alias)) { 47 | if (!instances[alias].isInitialized) { 48 | /* istanbul ignore next */ 49 | if (!Object.prototype.hasOwnProperty.call(initializePromises, alias)) { 50 | initializePromises[alias] = instances[alias].initialize() 51 | .catch((e) => { 52 | if (alias) { 53 | delete initializePromises[alias]; 54 | } 55 | 56 | throw e; 57 | }); 58 | } 59 | 60 | await initializePromises[alias]; 61 | } 62 | 63 | return instances[alias]; 64 | } 65 | 66 | /* istanbul ignore next */ 67 | if (!Object.prototype.hasOwnProperty.call(optionsPromises, alias)) { 68 | optionsPromises[alias] = useDataSourceOptions(alias) 69 | .catch((e) => { 70 | if (alias) { 71 | delete optionsPromises[alias]; 72 | } 73 | 74 | throw e; 75 | }); 76 | } 77 | 78 | const options = await optionsPromises[alias]; 79 | 80 | const dataSource = new DataSource(options); 81 | 82 | /* istanbul ignore next */ 83 | if (!Object.prototype.hasOwnProperty.call(initializePromises, alias)) { 84 | initializePromises[alias] = dataSource.initialize() 85 | .catch((e) => { 86 | if (alias) { 87 | delete initializePromises[alias]; 88 | } 89 | 90 | throw e; 91 | }); 92 | } 93 | 94 | await initializePromises[alias]; 95 | 96 | instances[alias] = dataSource; 97 | 98 | return dataSource; 99 | } 100 | -------------------------------------------------------------------------------- /src/env/constants.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023-2023. 3 | * Author Peter Placzek (tada5hi) 4 | * For the full copyright and license information, 5 | * view the LICENSE file that was distributed with this source code. 6 | */ 7 | 8 | export enum EnvironmentName { 9 | DEVELOPMENT = 'development', 10 | PRODUCTION = 'production', 11 | TEST = 'test', 12 | } 13 | 14 | export enum EnvironmentVariableName { 15 | ENV = 'NODE_ENV', 16 | 17 | // Seeder 18 | SEEDS = 'DB_SEEDS', 19 | SEEDS_ALT = 'TYPEORM_SEEDING_SEEDS', 20 | 21 | FACTORIES = 'DB_FACTORIES', 22 | FACTORIES_ALT = 'TYPEORM_SEEDING_FACTORIES', 23 | 24 | // Database 25 | TYPE = 'DB_TYPE', 26 | TYPE_ALT = 'TYPEORM_CONNECTION', 27 | 28 | URL = 'DB_URL', 29 | URL_ALT = 'TYPEORM_URL', 30 | 31 | HOST = 'DB_HOST', 32 | HOST_ALT = 'TYPEORM_HOST', 33 | 34 | PORT = 'DB_PORT', 35 | PORT_ALT = 'TYPEORM_PORT', 36 | 37 | USERNAME = 'DB_USERNAME', 38 | USERNAME_ALT = 'TYPEORM_USERNAME', 39 | 40 | PASSWORD = 'DB_PASSWORD', 41 | PASSWORD_ALT = 'TYPEORM_PASSWORD', 42 | 43 | DATABASE = 'DB_DATABASE', 44 | DATABASE_ALT = 'TYPEORM_DATABASE', 45 | 46 | SID = 'DB_SID', 47 | SID_ALT = 'TYPEORM_SID', 48 | 49 | SCHEMA = 'DB_SCHEMA', 50 | SCHEMA_ALT = 'TYPEORM_SCHEMA', 51 | 52 | SCHEMA_DROP = 'DB_DROP_SCHEMA', 53 | SCHEMA_DROP_ALT = 'TYPEORM_DROP_SCHEMA', 54 | 55 | DRIVER_EXTRA = 'DB_DRIVER_EXTRA', 56 | DRIVER_EXTRA_ALT = 'TYPEORM_DRIVER_EXTRA', 57 | 58 | SYNCHRONIZE = 'DB_SYNCHRONIZE', 59 | SYNCHRONIZE_ALT = 'TYPEORM_SYNCHRONIZE', 60 | 61 | MIGRATIONS = 'DB_MIGRATIONS', 62 | MIGRATIONS_ALT = 'TYPEORM_MIGRATIONS', 63 | 64 | MIGRATIONS_RUN = 'DB_MIGRATIONS_RUN', 65 | MIGRATIONS_RUN_ALT = 'TYPEORM_MIGRATIONS_RUN', 66 | 67 | MIGRATIONS_TABLE_NAME = 'DB_MIGRATIONS_TABLE_NAME', 68 | MIGRATIONS_TABLE_NAME_ALT = 'TYPEORM_MIGRATIONS_TABLE_NAME', 69 | 70 | ENTITIES = 'DB_ENTITIES', 71 | ENTITIES_ALT = 'TYPEORM_ENTITIES', 72 | 73 | ENTITY_PREFIX = 'DB_ENTITY_PREFIX', 74 | ENTITY_PREFIX_ALT = 'TYPEORM_ENTITY_PREFIX', 75 | 76 | METADATA_TABLE_NAME = 'DB_METADATA_TABLE_NAME', 77 | METADATA_TABLE_NAME_ALT = 'TYPEORM_METADATA_TABLE_NAME', 78 | 79 | SUBSCRIBERS = 'DB_SUBSCRIBERS', 80 | SUBSCRIBERS_ALT = 'TYPEORM_SUBSCRIBERS', 81 | 82 | LOGGING = 'DB_LOGGING', 83 | LOGGING_ALT = 'TYPEORM_LOGGING', 84 | 85 | LOGGER = 'DB_LOGGER', 86 | LOGGER_ALT = 'TYPEORM_LOGGER', 87 | 88 | MAX_QUERY_EXECUTION_TIME = 'DB_MAX_QUERY_EXECUTION_TIME', 89 | MAX_QUERY_EXECUTION_TIME_ALT = 'TYPEORM_MAX_QUERY_EXECUTION_TIME', 90 | 91 | DEBUG = 'DB_DEBUG', 92 | DEBUG_ALT = 'TYPEORM_DEBUG', 93 | 94 | UUID_EXTENSION = 'DB_UUID_EXTENSION', 95 | UUID_EXTENSION_ALT = 'TYPEORM_UUID_EXTENSION', 96 | 97 | CACHE = 'DB_CACHE', 98 | CACHE_ALT = 'TYPEORM_CACHE', 99 | 100 | CACHE_ALWAYS_ENABLED = 'DB_CACHE_ALWAYS_ENABLED', 101 | CACHE_ALWAYS_ENABLED_ALT = 'TYPEORM_CACHE_ALWAYS_ENABLED', 102 | 103 | CACHE_OPTIONS = 'DB_CACHE_OPTIONS', 104 | CACHE_OPTIONS_ALT = 'TYPEORM_CACHE_OPTIONS', 105 | 106 | CACHE_DURATION = 'DB_CACHE_DURATION', 107 | CACHE_DURATION_ALT = 'TYPEORM_CACHE_DURATION', 108 | } 109 | -------------------------------------------------------------------------------- /src/cli/commands/seed/run.ts: -------------------------------------------------------------------------------- 1 | import { consola } from 'consola'; 2 | import type { Arguments, Argv, CommandModule } from 'yargs'; 3 | import { buildDataSourceOptions, setDataSourceOptions, useDataSource } from '../../../data-source'; 4 | import { SeederExecutor } from '../../../seeder'; 5 | import { 6 | adjustFilePath, 7 | parseFilePath, 8 | readTSConfig, 9 | resolveFilePath, 10 | } from '../../../utils'; 11 | import type { TSConfig } from '../../../utils'; 12 | 13 | export interface SeedRunArguments extends Arguments { 14 | preserveFilePaths: boolean, 15 | root: string; 16 | tsconfig: string, 17 | dataSource: string; 18 | name?: string, 19 | } 20 | 21 | export class SeedRunCommand implements CommandModule { 22 | command = 'seed:run'; 23 | 24 | describe = 'Populate the database with an initial data set or generated data by a factory.'; 25 | 26 | builder(args: Argv) { 27 | return args 28 | .option('preserveFilePaths', { 29 | default: false, 30 | type: 'boolean', 31 | describe: 'This option indicates if file paths should be preserved.', 32 | }) 33 | .option('root', { 34 | alias: 'r', 35 | default: process.cwd(), 36 | describe: 'Root directory of the project.', 37 | }) 38 | .option('tsconfig', { 39 | alias: 'tc', 40 | default: 'tsconfig.json', 41 | describe: 'Name (or relative path incl. name) of the tsconfig file.', 42 | }) 43 | .option('dataSource', { 44 | alias: 'd', 45 | default: 'data-source', 46 | describe: 'Name (or relative path incl. name) of the data-source file.', 47 | }) 48 | .option('name', { 49 | alias: 'n', 50 | describe: 'Name (or relative path incl. name) of the seeder.', 51 | }); 52 | } 53 | 54 | async handler(raw: Arguments) { 55 | const args = raw as SeedRunArguments; 56 | 57 | let tsconfig : TSConfig | undefined; 58 | let sourcePath = resolveFilePath(args.dataSource, args.root); 59 | if (!args.preserveFilePaths) { 60 | tsconfig = await readTSConfig(args.root); 61 | sourcePath = await adjustFilePath(sourcePath, tsconfig); 62 | args.name = await adjustFilePath(args.name, tsconfig); 63 | } 64 | 65 | const source = parseFilePath(sourcePath); 66 | 67 | consola.info(`DataSource Directory: ${source.directory}`); 68 | consola.info(`DataSource Name: ${source.name}`); 69 | 70 | const dataSourceOptions = await buildDataSourceOptions({ 71 | dataSourceName: source.name, 72 | directory: source.directory, 73 | tsconfig, 74 | preserveFilePaths: args.preserveFilePaths, 75 | }); 76 | 77 | setDataSourceOptions(dataSourceOptions); 78 | 79 | if (args.name) { 80 | consola.info(`Seed Name: ${args.name}`); 81 | } 82 | 83 | const dataSource = await useDataSource(); 84 | const executor = new SeederExecutor(dataSource, { 85 | root: args.root, 86 | tsconfig, 87 | preserveFilePaths: args.preserveFilePaths, 88 | }); 89 | 90 | await executor.execute({ seedName: args.name }); 91 | 92 | process.exit(0); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/cli/commands/seed/create.ts: -------------------------------------------------------------------------------- 1 | import { getFileNameExtension, removeFileNameExtension } from 'locter'; 2 | import fs from 'node:fs'; 3 | import path from 'node:path'; 4 | import { pascalCase } from 'pascal-case'; 5 | import type { Arguments, Argv, CommandModule } from 'yargs'; 6 | import { consola } from 'consola'; 7 | import { buildSeederFileTemplate } from '../../../seeder'; 8 | import { isDirectory, parseFilePath } from '../../../utils'; 9 | 10 | export interface SeedCreateArguments extends Arguments { 11 | root: string; 12 | javascript: boolean; 13 | timestamp?: number, 14 | name: string 15 | } 16 | 17 | export class SeedCreateCommand implements CommandModule { 18 | command = 'seed:create'; 19 | 20 | describe = 'Create a seeder file.'; 21 | 22 | builder(args: Argv) { 23 | return args 24 | .option('root', { 25 | alias: 'r', 26 | default: process.cwd(), 27 | describe: 'Root directory of the project.', 28 | }) 29 | .option('timestamp', { 30 | alias: 't', 31 | type: 'number', 32 | describe: 'Custom timestamp for the seeder name.', 33 | }) 34 | .option('javascript', { 35 | alias: 'j', 36 | type: 'boolean', 37 | default: false, 38 | describe: 'Generate a seeder file for JavaScript instead of TypeScript.', 39 | }) 40 | .option('name', { 41 | alias: 'n', 42 | describe: 'Name (or relative path incl. name) of the seeder.', 43 | demandOption: true, 44 | }); 45 | } 46 | 47 | async handler(raw: Arguments) { 48 | const args = raw as SeedCreateArguments; 49 | 50 | let timestamp : number; 51 | if (Number.isNaN(args.timestamp) || !args.timestamp) { 52 | timestamp = Date.now(); 53 | } else { 54 | timestamp = args.timestamp; 55 | } 56 | 57 | const sourcePath = parseFilePath(args.name, args.root); 58 | 59 | const dirNameIsDirectory = await isDirectory(sourcePath.directory); 60 | if (!dirNameIsDirectory) { 61 | consola.warn(`The output directory ${sourcePath.directory} does not exist.`); 62 | process.exit(1); 63 | } 64 | 65 | const extension = args.javascript ? 66 | '.js' : 67 | '.ts'; 68 | 69 | const nameExtension = getFileNameExtension(sourcePath.name); 70 | const nameWithoutExtension = removeFileNameExtension(sourcePath.name); 71 | 72 | let fileName: string; 73 | if (nameExtension) { 74 | fileName = `${timestamp}-${sourcePath.name}`; 75 | } else { 76 | fileName = `${timestamp}-${sourcePath.name}${extension}`; 77 | } 78 | const filePath = sourcePath.directory + path.sep + fileName; 79 | const template = buildSeederFileTemplate(nameWithoutExtension, timestamp); 80 | 81 | consola.info(`Seed Directory: ${sourcePath.directory}`); 82 | consola.info(`Seed FileName: ${fileName}`); 83 | consola.info(`Seed Name: ${pascalCase(nameWithoutExtension)}`); 84 | 85 | try { 86 | await fs.promises.writeFile(filePath, template, { encoding: 'utf-8' }); 87 | } catch (e) { 88 | consola.warn(`The seed could not be written to the path ${filePath}.`); 89 | process.exit(1); 90 | } 91 | 92 | process.exit(0); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/cli/commands/database/drop.ts: -------------------------------------------------------------------------------- 1 | import { consola } from 'consola'; 2 | import type { Arguments, Argv, CommandModule } from 'yargs'; 3 | import { buildDataSourceOptions } from '../../../data-source'; 4 | import type { DatabaseDropContext } from '../../../database'; 5 | import { dropDatabase } from '../../../database'; 6 | import { 7 | adjustFilePath, 8 | parseFilePath, 9 | readTSConfig, 10 | resolveFilePath, 11 | } from '../../../utils'; 12 | import type { TSConfig } from '../../../utils'; 13 | 14 | export interface DatabaseDropArguments extends Arguments { 15 | preserveFilePaths: boolean, 16 | root: string; 17 | tsconfig: string, 18 | dataSource: string; 19 | } 20 | 21 | export class DatabaseDropCommand implements CommandModule { 22 | command = 'db:drop'; 23 | 24 | describe = 'Drop database.'; 25 | 26 | builder(args: Argv) { 27 | return args 28 | .option('preserveFilePaths', { 29 | default: false, 30 | type: 'boolean', 31 | describe: 'This option indicates if file paths should be preserved.', 32 | }) 33 | .option('root', { 34 | alias: 'r', 35 | default: process.cwd(), 36 | describe: 'Root directory of the project.', 37 | }) 38 | .option('tsconfig', { 39 | alias: 'tc', 40 | default: 'tsconfig.json', 41 | describe: 'Name (or relative path incl. name) of the tsconfig file.', 42 | }) 43 | .option('dataSource', { 44 | alias: 'd', 45 | default: 'data-source', 46 | describe: 'Name (or relative path incl. name) of the data-source file.', 47 | }) 48 | .option('initialDatabase', { 49 | describe: 'Specify the initial database to connect to.', 50 | }); 51 | } 52 | 53 | async handler(raw: Arguments) { 54 | const args : DatabaseDropArguments = raw as DatabaseDropArguments; 55 | 56 | let tsconfig : TSConfig | undefined; 57 | let sourcePath = resolveFilePath(args.dataSource, args.root); 58 | if (!args.preserveFilePaths) { 59 | tsconfig = await readTSConfig(resolveFilePath(args.root, args.tsconfig)); 60 | sourcePath = await adjustFilePath(sourcePath, tsconfig); 61 | } 62 | 63 | const source = parseFilePath(sourcePath); 64 | 65 | consola.info(`DataSource Directory: ${source.directory}`); 66 | consola.info(`DataSource Name: ${source.name}`); 67 | 68 | const dataSourceOptions = await buildDataSourceOptions({ 69 | directory: source.directory, 70 | dataSourceName: source.name, 71 | tsconfig, 72 | preserveFilePaths: args.preserveFilePaths, 73 | }); 74 | 75 | const context : DatabaseDropContext = { 76 | ifExist: true, 77 | options: dataSourceOptions, 78 | }; 79 | 80 | if ( 81 | typeof args.initialDatabase === 'string' && 82 | args.initialDatabase !== '' 83 | ) { 84 | context.initialDatabase = args.initialDatabase; 85 | } 86 | 87 | try { 88 | await dropDatabase(context); 89 | consola.success('Dropped database.'); 90 | process.exit(0); 91 | } catch (e) { 92 | consola.warn('Failed to drop database.'); 93 | consola.error(e); 94 | process.exit(1); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typeorm-extension", 3 | "version": "3.7.3", 4 | "description": "A library to create/drop database, simple seeding data sets, ...", 5 | "author": { 6 | "name": "Peter Placzek", 7 | "email": "contact@tada5hi.net", 8 | "url": "https://github.com/tada5hi" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/tada5hi/typeorm-extension.git" 13 | }, 14 | "main": "dist/index.cjs", 15 | "module": "dist/index.mjs", 16 | "types": "dist/index.d.ts", 17 | "exports": { 18 | "./package.json": "./package.json", 19 | ".": { 20 | "types": "./dist/index.d.ts", 21 | "import": "./dist/index.mjs", 22 | "require": "./dist/index.cjs" 23 | }, 24 | "./bin/*": "./bin/*" 25 | }, 26 | "files": [ 27 | "bin", 28 | "dist" 29 | ], 30 | "engines": { 31 | "node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0" 32 | }, 33 | "scripts": { 34 | "build:types": "tsc --emitDeclarationOnly", 35 | "build:js": "rollup -c", 36 | "build": "rimraf dist && rimraf bin && npm run build:types && npm run build:js", 37 | "build:watch": "npm run build -- --watch", 38 | "commit": "npx git-cz", 39 | "test": "jest --config ./test/jest.config.js", 40 | "test:coverage": "cross-env NODE_ENV=test jest --config ./test/jest.config.js --coverage", 41 | "lint": "eslint --ext .js,.vue,.ts ./src ./test", 42 | "lint:fix": "npm run lint -- --fix", 43 | "docs:dev": "vitepress dev docs --temp .temp", 44 | "docs:build": "vitepress build docs --temp .temp", 45 | "docs:help": "vitepress --help", 46 | "prepare": "husky install" 47 | }, 48 | "keywords": [ 49 | "database", 50 | "create", 51 | "drop", 52 | "api", 53 | "json-api", 54 | "jsonapi", 55 | "migration", 56 | "seeder", 57 | "seeding", 58 | "cli" 59 | ], 60 | "bin": { 61 | "typeorm-extension": "bin/cli.cjs", 62 | "typeorm-extension-esm": "bin/cli.mjs" 63 | }, 64 | "license": "MIT", 65 | "dependencies": { 66 | "consola": "^3.4.0", 67 | "envix": "^1.5.0", 68 | "locter": "^2.2.1", 69 | "pascal-case": "^3.1.2", 70 | "rapiq": "^0.9.0", 71 | "reflect-metadata": "^0.2.2", 72 | "smob": "^1.5.0", 73 | "yargs": "^18.0.0" 74 | }, 75 | "peerDependencies": { 76 | "@faker-js/faker": ">=8.4.1", 77 | "typeorm": "~0.3.0" 78 | }, 79 | "devDependencies": { 80 | "@faker-js/faker": "^9.9.0", 81 | "@rollup/plugin-node-resolve": "^16.0.3", 82 | "@swc/core": "^1.15.3", 83 | "@tada5hi/commitlint-config": "^1.2.6", 84 | "@tada5hi/eslint-config-typescript": "^1.2.17", 85 | "@tada5hi/tsconfig": "^0.6.0", 86 | "@types/jest": "^30.0.0", 87 | "@types/node": "^24.10.1", 88 | "better-sqlite3": "^12.4.6", 89 | "cross-env": "^10.1.0", 90 | "eslint": "^8.56.0", 91 | "husky": "^9.1.7", 92 | "jest": "^30.2.0", 93 | "rimraf": "^6.1.2", 94 | "rollup": "^4.53.3", 95 | "ts-jest": "^29.4.5", 96 | "typeorm": "^0.3.27", 97 | "typescript": "^5.9.3", 98 | "vitepress": "^1.6.4", 99 | "vue": "^3.5.25", 100 | "workspaces-publish": "^1.5.0" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /docs/guide/cli.md: -------------------------------------------------------------------------------- 1 | # CLI 2 | 3 | If you use esm, the executable must be changed from `typeorm-extension` to `typeorm-extension-esm`. 4 | The following commands are available in the terminal: 5 | - `typeorm-extension db:create` to create the database 6 | - `typeorm-extension db:drop` to drop the database 7 | - `typeorm-extension seed:run` seed the database 8 | - `typeorm-extension seed:create` to create a new seeder 9 | 10 | If the application has not yet been built or is to be tested with ts-node, the commands can be adapted as follows: 11 | 12 | ``` 13 | "scripts": { 14 | "db:create": "ts-node ./node_modules/typeorm-extension/bin/cli.cjs db:create", 15 | "db:drop": "ts-node ./node_modules/typeorm-extension/bin/cli.cjs db:drop", 16 | "seed:run": "ts-node ./node_modules/typeorm-extension/bin/cli.cjs seed:run", 17 | "seed:create": "ts-node ./node_modules/typeorm-extension/bin/cli.cjs seed:create" 18 | } 19 | ``` 20 | To test the application in the context of an esm project, the following adjustments must be made: 21 | - executable `ts-node` to `ts-node-esm` 22 | - library path `cli.cjs` to `cli.mjs` 23 | 24 | Read the [Seeding Configuration](./seeding#configuration) section to find out how to specify the path, 25 | for the seeder- & factory-location. 26 | 27 | #### CLI Options 28 | 29 | | Option | Commands | Default | Description | 30 | |-------------------------|----------------------------------------------------|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 31 | | `--root` or `-r` | `db:create`, `db:drop`, `seed:create` & `seed:run` | `process.cwd()` | Root directory of the project. | 32 | | `--dataSource` or `-d` | `db:create`, `db:drop` & `seed:run` | `data-source` | Name (or relative path incl. name) of the data-source file. | 33 | | `--synchronize` or `-s` | `db:create` | `yes` | Synchronize the database schema after database creation. Options: `yes` or `no`. | 34 | | `--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. | 35 | | `--name` | `seed:create` & `seed:run` | `undefined` | Name (or relative path incl. name) of the seeder. | 36 | | `--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. | 37 | -------------------------------------------------------------------------------- /docs/guide/database.md: -------------------------------------------------------------------------------- 1 | # Database 2 | An alternative to the CLI variant, is to `create` the database in the code base during the runtime of the application. 3 | Therefore, provide the `DataSourceOptions` for the DataSource manually, or let it be created automatically: 4 | 5 | ## Create 6 | **`Example #1`** 7 | ```typescript 8 | import { 9 | DataSource, 10 | DataSourceOptions 11 | } from 'typeorm'; 12 | import { createDatabase } from 'typeorm-extension'; 13 | 14 | (async () => { 15 | const options: DataSourceOptions = { 16 | type: 'better-sqlite', 17 | database: 'db.sqlite' 18 | }; 19 | 20 | // Create the database with specification of the DataSource options 21 | await createDatabase({ 22 | options 23 | }); 24 | 25 | const dataSource = new DataSource(options); 26 | await dataSource.initialize(); 27 | // do something with the DataSource 28 | })(); 29 | ``` 30 | 31 | **`Example #2`** 32 | ```typescript 33 | import { 34 | buildDataSourceOptions, 35 | createDatabase 36 | } from 'typeorm-extension'; 37 | 38 | (async () => { 39 | const options = await buildDataSourceOptions(); 40 | 41 | // modify options 42 | 43 | // Create the database with specification of the DataSource options 44 | await createDatabase({ 45 | options 46 | }); 47 | 48 | const dataSource = new DataSource(options); 49 | await dataSource.initialize(); 50 | // do something with the DataSource 51 | })(); 52 | ``` 53 | 54 | **`Example #3`** 55 | 56 | It is also possible to let the library automatically search for the data-source under the hood. 57 | Therefore, it will search by default for a `data-source.{ts,js}` file in the following directories: 58 | - `{src,dist}/db/` 59 | - `{src,dist}/database` 60 | - `{src,dist}` 61 | 62 | ```typescript 63 | import { createDatabase } from 'typeorm-extension'; 64 | 65 | (async () => { 66 | // Create the database without specifying it manually 67 | await createDatabase(); 68 | })(); 69 | ``` 70 | 71 | 72 | To get a better overview and understanding of the [createDatabase](#createdatabase) function go to the [functions](#functions---database) section and read more about it. 73 | 74 | ## Drop 75 | 76 | **`Example #1`** 77 | ```typescript 78 | import { 79 | DataSource, 80 | DataSourceOptions 81 | } from 'typeorm'; 82 | import { dropDatabase } from 'typeorm-extension'; 83 | 84 | (async () => { 85 | const options: DataSourceOptions = { 86 | type: 'better-sqlite', 87 | database: 'db.sqlite' 88 | }; 89 | 90 | // Create the database with specification of the DataSource options 91 | await dropDatabase({ 92 | options 93 | }); 94 | })(); 95 | ``` 96 | 97 | **`Example #2`** 98 | ```typescript 99 | import { 100 | buildDataSourceOptions, 101 | dropDatabase 102 | } from 'typeorm-extension'; 103 | 104 | (async () => { 105 | const options = await buildDataSourceOptions(); 106 | 107 | // modify options 108 | 109 | // Drop the database with specification of the DataSource options 110 | await dropDatabase({ 111 | options 112 | }); 113 | })(); 114 | ``` 115 | 116 | **`Example #3`** 117 | 118 | It is also possible to let the library automatically search for the data-source under the hood. 119 | Therefore, it will search by default for a `data-source.{ts,js}` file in the following directories: 120 | - `{src,dist}/db/` 121 | - `{src,dist}/database` 122 | - `{src,dist}` 123 | 124 | ```typescript 125 | import { dropDatabase } from 'typeorm-extension'; 126 | 127 | (async () => { 128 | // Drop the database without specifying it manually 129 | await dropDatabase(); 130 | })(); 131 | ``` 132 | 133 | To get a better overview and understanding of the [dropDatabase](#dropdatabase) function go to the [functions](#functions---database) section and read more about it. 134 | -------------------------------------------------------------------------------- /src/data-source/find/module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isObject, 3 | load, 4 | locate, 5 | removeFileNameExtension, 6 | } from 'locter'; 7 | import path from 'node:path'; 8 | import type { DataSource } from 'typeorm'; 9 | import { InstanceChecker } from 'typeorm'; 10 | import { 11 | adjustFilePath, 12 | isPromise, 13 | readTSConfig, 14 | safeReplaceWindowsSeparator, 15 | } from '../../utils'; 16 | import type { DataSourceFindOptions } from './type'; 17 | import type { TSConfig } from '../../utils'; 18 | 19 | export async function findDataSource( 20 | context: DataSourceFindOptions = {}, 21 | ) : Promise { 22 | let tsconfig : TSConfig | undefined; 23 | if (!context.preserveFilePaths) { 24 | if (isObject(context.tsconfig)) { 25 | tsconfig = context.tsconfig; 26 | } else { 27 | tsconfig = await readTSConfig(context.tsconfig); 28 | } 29 | } 30 | 31 | const files : string[] = [ 32 | 'data-source', 33 | ]; 34 | 35 | if (context.fileName) { 36 | context.fileName = removeFileNameExtension( 37 | context.fileName, 38 | ['.ts', '.mts', '.cts', '.js', '.mjs', '.cjs'], 39 | ); 40 | 41 | if (context.fileName !== 'data-source') { 42 | files.unshift(context.fileName); 43 | } 44 | } 45 | 46 | let { directory } = context; 47 | let directoryIsPattern = false; 48 | if (context.directory) { 49 | if (path.isAbsolute(context.directory)) { 50 | directory = context.directory; 51 | } else { 52 | directoryIsPattern = true; 53 | directory = safeReplaceWindowsSeparator(context.directory); 54 | } 55 | 56 | if (!context.preserveFilePaths) { 57 | directory = await adjustFilePath(directory, tsconfig); 58 | } 59 | } 60 | 61 | const lookupPaths = []; 62 | for (let j = 0; j < files.length; j++) { 63 | if ( 64 | directory && 65 | directoryIsPattern 66 | ) { 67 | lookupPaths.push(path.posix.join(directory, files[j])); 68 | } 69 | 70 | lookupPaths.push(...[ 71 | path.posix.join('src', files[j]), 72 | path.posix.join('src/{db,database}', files[j]), 73 | ]); 74 | } 75 | 76 | files.push(...lookupPaths); 77 | 78 | if (!context.preserveFilePaths) { 79 | for (let j = 0; j < files.length; j++) { 80 | files[j] = await adjustFilePath(files[j], tsconfig); 81 | } 82 | } 83 | 84 | for (let i = 0; i < files.length; i++) { 85 | const info = await locate( 86 | `${files[i]}.{js,cjs,mjs,ts,cts,mts}`, 87 | { 88 | path: [ 89 | process.cwd(), 90 | ...(directory && !directoryIsPattern ? [directory] : []), 91 | ], 92 | ignore: ['**/*.d.ts'], 93 | }, 94 | ); 95 | 96 | if (info) { 97 | let moduleRecord = await load(info); 98 | 99 | if (isPromise(moduleRecord)) { 100 | moduleRecord = await moduleRecord; 101 | } 102 | 103 | if (InstanceChecker.isDataSource(moduleRecord)) { 104 | return moduleRecord; 105 | } 106 | 107 | if (!isObject(moduleRecord)) { 108 | continue; 109 | } 110 | 111 | const keys = Object.keys(moduleRecord); 112 | for (let j = 0; j < keys.length; j++) { 113 | let value = moduleRecord[keys[j]]; 114 | 115 | if (isPromise(value)) { 116 | value = await value; 117 | } 118 | 119 | if (InstanceChecker.isDataSource(value)) { 120 | return value; 121 | } 122 | } 123 | } 124 | } 125 | 126 | return undefined; 127 | } 128 | -------------------------------------------------------------------------------- /src/database/driver/mysql.ts: -------------------------------------------------------------------------------- 1 | import type { MysqlDriver } from 'typeorm/driver/mysql/MysqlDriver'; 2 | import type { 3 | DatabaseCreateContextInput, 4 | DatabaseDropContextInput, 5 | } from '../methods'; 6 | import type { DriverOptions } from './types'; 7 | import { buildDriverOptions, createDriver } from './utils'; 8 | import { buildDatabaseCreateContext, buildDatabaseDropContext, synchronizeDatabaseSchema } from '../utils'; 9 | 10 | export async function createSimpleMySQLConnection( 11 | driver: MysqlDriver, 12 | options: DriverOptions, 13 | ) { 14 | /** 15 | * mysql|mysql2 library 16 | */ 17 | const { createConnection } = driver.mysql; 18 | 19 | const option : Record = { 20 | host: options.host, 21 | user: options.user, 22 | password: options.password, 23 | port: options.port, 24 | ssl: options.ssl, 25 | ...(options.extra ? options.extra : {}), 26 | }; 27 | 28 | return createConnection(option); 29 | } 30 | 31 | export async function executeSimpleMysqlQuery(connection: any, query: string, endConnection = true) { 32 | return new Promise(((resolve, reject) => { 33 | connection.query(query, (queryErr: any, queryResult: any) => { 34 | if (endConnection) connection.end(); 35 | 36 | if (queryErr) { 37 | reject(queryErr); 38 | } 39 | 40 | resolve(queryResult); 41 | }); 42 | })); 43 | } 44 | 45 | export async function createMySQLDatabase( 46 | input: DatabaseCreateContextInput = {}, 47 | ) { 48 | const context = await buildDatabaseCreateContext(input); 49 | const options = buildDriverOptions(context.options); 50 | const driver = createDriver(context.options) as MysqlDriver; 51 | 52 | const connection = await createSimpleMySQLConnection(driver, options); 53 | /** 54 | * @link https://github.com/typeorm/typeorm/blob/master/src/driver/mysql/MysqlQueryRunner.ts#L297 55 | */ 56 | let query = context.ifNotExist ? 57 | `CREATE DATABASE IF NOT EXISTS \`${options.database}\`` : 58 | `CREATE DATABASE \`${options.database}\``; 59 | 60 | if (typeof options.charset === 'string') { 61 | const { charset } = options; 62 | let { characterSet } = options; 63 | 64 | if (typeof characterSet === 'undefined') { 65 | if (charset.toLowerCase().startsWith('utf8mb4')) { 66 | characterSet = 'utf8mb4'; 67 | } else if (charset.toLowerCase().startsWith('utf8')) { 68 | characterSet = 'utf8'; 69 | } 70 | } 71 | 72 | if (typeof characterSet === 'string') { 73 | query += ` CHARACTER SET ${characterSet} COLLATE ${charset}`; 74 | } 75 | } 76 | 77 | const result = await executeSimpleMysqlQuery(connection, query); 78 | 79 | if (context.synchronize) { 80 | await synchronizeDatabaseSchema(context.options); 81 | } 82 | 83 | return result; 84 | } 85 | 86 | export async function dropMySQLDatabase( 87 | input: DatabaseDropContextInput = {}, 88 | ) { 89 | const context = await buildDatabaseDropContext(input); 90 | const options = buildDriverOptions(context.options); 91 | const driver = createDriver(context.options) as MysqlDriver; 92 | 93 | const connection = await createSimpleMySQLConnection(driver, options); 94 | 95 | /** 96 | * @link https://github.com/typeorm/typeorm/blob/master/src/driver/mysql/MysqlQueryRunner.ts#L306 97 | */ 98 | const query = context.ifExist ? 99 | `DROP DATABASE IF EXISTS \`${options.database}\`` : 100 | `DROP DATABASE \`${options.database}\``; 101 | 102 | await executeSimpleMysqlQuery(connection, 'SET FOREIGN_KEY_CHECKS=0;', false); 103 | const result = await executeSimpleMysqlQuery(connection, query, false); 104 | await executeSimpleMysqlQuery(connection, 'SET FOREIGN_KEY_CHECKS=1;'); 105 | return result; 106 | } 107 | -------------------------------------------------------------------------------- /src/cli/commands/database/create.ts: -------------------------------------------------------------------------------- 1 | import { consola } from 'consola'; 2 | import type { Arguments, Argv, CommandModule } from 'yargs'; 3 | import { buildDataSourceOptions } from '../../../data-source'; 4 | import type { DatabaseCreateContextInput } from '../../../database'; 5 | import { createDatabase } from '../../../database'; 6 | import { 7 | adjustFilePath, 8 | parseFilePath, 9 | readTSConfig, 10 | resolveFilePath, 11 | } from '../../../utils'; 12 | import type { TSConfig } from '../../../utils'; 13 | 14 | export interface DatabaseCreateArguments extends Arguments { 15 | preserveFilePaths: boolean, 16 | root: string; 17 | tsconfig: string, 18 | dataSource: string; 19 | synchronize: string; 20 | initialDatabase?: unknown; 21 | } 22 | 23 | export class DatabaseCreateCommand implements CommandModule { 24 | command = 'db:create'; 25 | 26 | describe = 'Create database.'; 27 | 28 | builder(args: Argv) { 29 | return args 30 | .option('preserveFilePaths', { 31 | default: false, 32 | type: 'boolean', 33 | describe: 'This option indicates if file paths should be preserved.', 34 | }) 35 | .option('root', { 36 | alias: 'r', 37 | default: process.cwd(), 38 | describe: 'Root directory of the project.', 39 | }) 40 | .option('tsconfig', { 41 | alias: 'tc', 42 | default: 'tsconfig.json', 43 | describe: 'Name (or relative path incl. name) of the tsconfig file.', 44 | }) 45 | .option('dataSource', { 46 | alias: 'd', 47 | default: 'data-source', 48 | describe: 'Name (or relative path incl. name) of the data-source file.', 49 | }) 50 | .option('synchronize', { 51 | alias: 's', 52 | default: 'yes', 53 | describe: 'Create database schema for all entities.', 54 | choices: ['yes', 'no'], 55 | }) 56 | .option('initialDatabase', { 57 | describe: 'Specify the initial database to connect to.', 58 | }); 59 | } 60 | 61 | async handler(raw: Arguments) { 62 | const args : DatabaseCreateArguments = raw as DatabaseCreateArguments; 63 | 64 | let tsconfig : TSConfig | undefined; 65 | let sourcePath = resolveFilePath(args.dataSource, args.root); 66 | if (!args.preserveFilePaths) { 67 | tsconfig = await readTSConfig(resolveFilePath(args.root, args.tsconfig)); 68 | sourcePath = await adjustFilePath(sourcePath, tsconfig); 69 | } 70 | 71 | const source = parseFilePath(sourcePath); 72 | 73 | consola.info(`DataSource Directory: ${source.directory}`); 74 | consola.info(`DataSource Name: ${source.name}`); 75 | 76 | const dataSourceOptions = await buildDataSourceOptions({ 77 | directory: source.directory, 78 | dataSourceName: source.name, 79 | tsconfig, 80 | preserveFilePaths: args.preserveFilePaths, 81 | }); 82 | 83 | const context : DatabaseCreateContextInput = { 84 | ifNotExist: true, 85 | options: dataSourceOptions, 86 | synchronize: args.synchronize === 'yes', 87 | }; 88 | 89 | if ( 90 | typeof args.initialDatabase === 'string' && 91 | args.initialDatabase !== '' 92 | ) { 93 | context.initialDatabase = args.initialDatabase; 94 | } 95 | 96 | try { 97 | await createDatabase(context); 98 | consola.success('Created database.'); 99 | process.exit(0); 100 | } catch (e) { 101 | consola.warn('Failed to create database.'); 102 | consola.error(e); 103 | process.exit(1); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/data-source/options/utils/env.ts: -------------------------------------------------------------------------------- 1 | import type { DataSourceOptions } from 'typeorm'; 2 | import type { BaseDataSourceOptions } from 'typeorm/data-source/BaseDataSourceOptions'; 3 | import type { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions'; 4 | import type { DatabaseType } from 'typeorm/driver/types/DatabaseType'; 5 | import type { LoggerOptions } from 'typeorm/logger/LoggerOptions'; 6 | import { useEnv } from '../../../env'; 7 | import { mergeDataSourceOptions } from './merge'; 8 | 9 | export function hasEnvDataSourceOptions() : boolean { 10 | return !!useEnv('type'); 11 | } 12 | 13 | /* istanbul ignore next */ 14 | export function readDataSourceOptionsFromEnv() : DataSourceOptions | undefined { 15 | if (!hasEnvDataSourceOptions()) { 16 | return undefined; 17 | } 18 | 19 | // todo: include seeder options 20 | const base : Omit = { 21 | type: useEnv('type') as DatabaseType, 22 | entities: useEnv('entities'), 23 | subscribers: useEnv('subscribers'), 24 | migrations: useEnv('migrations'), 25 | migrationsTableName: useEnv('migrationsTableName'), 26 | // migrationsTransactionMode: useEnv('migra') 27 | metadataTableName: useEnv('metadataTableName'), 28 | logging: useEnv('logging') as LoggerOptions, 29 | logger: useEnv('logger') as BaseDataSourceOptions['logger'], 30 | maxQueryExecutionTime: useEnv('maxQueryExecutionTime'), 31 | synchronize: useEnv('synchronize'), 32 | migrationsRun: useEnv('migrationsRun'), 33 | dropSchema: useEnv('schemaDrop'), 34 | entityPrefix: useEnv('entityPrefix'), 35 | extra: useEnv('extra'), 36 | cache: useEnv('cache'), 37 | }; 38 | 39 | const credentialOptions = { 40 | url: useEnv('url'), 41 | host: useEnv('host'), 42 | port: useEnv('port'), 43 | username: useEnv('username'), 44 | password: useEnv('password'), 45 | database: useEnv('database'), 46 | }; 47 | 48 | if (base.type === 'mysql' || base.type === 'mariadb') { 49 | return { 50 | ...base, 51 | ...credentialOptions, 52 | type: base.type, 53 | }; 54 | } 55 | 56 | if (base.type === 'postgres') { 57 | return { 58 | ...base, 59 | ...credentialOptions, 60 | type: base.type, 61 | schema: useEnv('schema'), 62 | uuidExtension: useEnv('uuidExtension') as PostgresConnectionOptions['uuidExtension'], 63 | }; 64 | } 65 | 66 | if (base.type === 'cockroachdb') { 67 | return { 68 | ...base, 69 | ...credentialOptions, 70 | type: base.type, 71 | schema: useEnv('schema'), 72 | timeTravelQueries: true, 73 | }; 74 | } 75 | 76 | if (base.type === 'sqlite') { 77 | return { 78 | ...base, 79 | type: base.type, 80 | database: useEnv('database') || 'db.sqlite', 81 | }; 82 | } 83 | 84 | if (base.type === 'better-sqlite3') { 85 | return { 86 | ...base, 87 | type: base.type, 88 | database: useEnv('database') || 'db.sqlite', 89 | }; 90 | } 91 | 92 | if (base.type === 'mssql') { 93 | return { 94 | ...base, 95 | ...credentialOptions, 96 | type: base.type, 97 | schema: useEnv('schema'), 98 | }; 99 | } 100 | 101 | if (base.type === 'oracle') { 102 | return { 103 | ...base, 104 | ...credentialOptions, 105 | type: base.type, 106 | sid: useEnv('sid'), 107 | }; 108 | } 109 | 110 | return { 111 | ...base, 112 | ...credentialOptions, 113 | } as DataSourceOptions; 114 | } 115 | 116 | export function mergeDataSourceOptionsWithEnv(options: DataSourceOptions) { 117 | const env = readDataSourceOptionsFromEnv(); 118 | if (!env) { 119 | return options; 120 | } 121 | 122 | return mergeDataSourceOptions(env, options); 123 | } 124 | -------------------------------------------------------------------------------- /src/database/methods/check/module.ts: -------------------------------------------------------------------------------- 1 | import type { DataSourceOptions } from 'typeorm'; 2 | import { DataSource, MigrationExecutor } from 'typeorm'; 3 | import { 4 | hasDataSource, 5 | useDataSource, 6 | useDataSourceOptions, 7 | } from '../../../data-source'; 8 | import { hasStringProperty } from '../../../utils'; 9 | import type { DatabaseCheckContext, DatabaseCheckResult } from './types'; 10 | 11 | /** 12 | * Check database setup progress. 13 | * 14 | * @param context 15 | */ 16 | export async function checkDatabase(context: DatabaseCheckContext = {}) : Promise { 17 | const result : DatabaseCheckResult = { 18 | exists: true, 19 | schema: false, 20 | migrationsPending: [], 21 | }; 22 | 23 | let dataSource : DataSource; 24 | let dataSourceCleanup : boolean; 25 | 26 | if ( 27 | typeof context.dataSource === 'undefined' && 28 | typeof context.options === 'undefined' && 29 | hasDataSource(context.alias) 30 | ) { 31 | dataSource = await useDataSource(context.alias); 32 | 33 | if ( 34 | dataSource.options.synchronize || 35 | dataSource.options.migrationsRun 36 | ) { 37 | dataSource = new DataSource({ 38 | ...dataSource.options, 39 | synchronize: false, 40 | migrationsRun: false, 41 | }); 42 | 43 | dataSourceCleanup = true; 44 | } else { 45 | dataSourceCleanup = false; 46 | } 47 | } else { 48 | let dataSourceOptions : DataSourceOptions; 49 | if (context.options) { 50 | dataSourceOptions = context.options; 51 | } else { 52 | dataSourceOptions = await useDataSourceOptions(context.alias); 53 | } 54 | 55 | dataSource = new DataSource({ 56 | ...dataSourceOptions, 57 | synchronize: false, 58 | migrationsRun: false, 59 | }); 60 | dataSourceCleanup = context.dataSourceCleanup ?? true; 61 | } 62 | 63 | try { 64 | if (!dataSource.isInitialized) { 65 | await dataSource.initialize(); 66 | } 67 | } catch (e) { 68 | result.exists = false; 69 | 70 | return result; 71 | } 72 | 73 | const queryRunner = dataSource.createQueryRunner(); 74 | 75 | if ( 76 | dataSource.migrations && 77 | dataSource.migrations.length > 0 78 | ) { 79 | const migrationExecutor = new MigrationExecutor(dataSource, queryRunner); 80 | result.migrationsPending = await migrationExecutor.getPendingMigrations(); 81 | 82 | result.schema = result.migrationsPending.length === 0; 83 | } else { 84 | let schema : string | undefined; 85 | if (hasStringProperty(dataSource.driver.options, 'schema')) { 86 | schema = dataSource.driver.options.schema; 87 | } 88 | 89 | const migrationsTableName = dataSource.driver.buildTableName( 90 | dataSource.options.migrationsTableName || 'migrations', 91 | schema, 92 | dataSource.driver.database, 93 | ); 94 | const migrationsTableExists = await queryRunner.hasTable(migrationsTableName); 95 | if (migrationsTableExists) { 96 | result.schema = dataSource.entityMetadatas.length === 0; 97 | } else { 98 | const tableNames = dataSource.entityMetadatas.map( 99 | (entityMetadata) => entityMetadata.tablePath, 100 | ); 101 | const tables = await queryRunner.getTables(tableNames); 102 | 103 | if (tables.length === dataSource.entityMetadatas.length) { 104 | const { upQueries } = await dataSource.driver.createSchemaBuilder() 105 | .log(); 106 | 107 | result.schema = upQueries.length === 0; 108 | } else { 109 | result.schema = false; 110 | } 111 | } 112 | } 113 | 114 | await queryRunner.release(); 115 | 116 | if (dataSourceCleanup) { 117 | await dataSource.destroy(); 118 | } 119 | 120 | return result; 121 | } 122 | -------------------------------------------------------------------------------- /src/database/utils/migration.ts: -------------------------------------------------------------------------------- 1 | import { pascalCase } from 'pascal-case'; 2 | import path from 'node:path'; 3 | import fs from 'node:fs'; 4 | import process from 'node:process'; 5 | import { MigrationGenerateCommand } from 'typeorm/commands/MigrationGenerateCommand'; 6 | import type { MigrationGenerateCommandContext, MigrationGenerateResult } from './type'; 7 | 8 | class GenerateCommand extends MigrationGenerateCommand { 9 | static prettify(query: string) { 10 | return this.prettifyQuery(query); 11 | } 12 | } 13 | 14 | function queryParams(parameters: any[] | undefined): string { 15 | if (!parameters || !parameters.length) { 16 | return ''; 17 | } 18 | 19 | return `, ${JSON.stringify(parameters)}`; 20 | } 21 | 22 | function buildTemplate( 23 | name: string, 24 | timestamp: number, 25 | upStatements: string[], 26 | downStatements: string[], 27 | ): string { 28 | const migrationName = `${pascalCase(name)}${timestamp}`; 29 | 30 | const up = upStatements.map((statement) => ` ${statement}`); 31 | const down = downStatements.map((statement) => ` ${statement}`); 32 | 33 | return `import { MigrationInterface, QueryRunner } from 'typeorm'; 34 | 35 | export class ${migrationName} implements MigrationInterface { 36 | name = '${migrationName}'; 37 | 38 | public async up(queryRunner: QueryRunner): Promise { 39 | ${up.join(` 40 | `)} 41 | } 42 | public async down(queryRunner: QueryRunner): Promise { 43 | ${down.join(` 44 | `)} 45 | } 46 | } 47 | `; 48 | } 49 | 50 | export async function generateMigration( 51 | context: MigrationGenerateCommandContext, 52 | ) : Promise { 53 | context.name = context.name || 'Default'; 54 | 55 | const timestamp = context.timestamp || new Date().getTime(); 56 | const fileName = `${timestamp}-${context.name}.ts`; 57 | 58 | const { dataSource } = context; 59 | 60 | const up: string[] = []; const 61 | down: string[] = []; 62 | 63 | if (!dataSource.isInitialized) { 64 | await dataSource.initialize(); 65 | } 66 | 67 | const sqlInMemory = await dataSource.driver.createSchemaBuilder().log(); 68 | 69 | if (context.prettify) { 70 | sqlInMemory.upQueries.forEach((upQuery) => { 71 | upQuery.query = GenerateCommand.prettify( 72 | upQuery.query, 73 | ); 74 | }); 75 | sqlInMemory.downQueries.forEach((downQuery) => { 76 | downQuery.query = GenerateCommand.prettify( 77 | downQuery.query, 78 | ); 79 | }); 80 | } 81 | 82 | sqlInMemory.upQueries.forEach((upQuery) => { 83 | up.push(`await queryRunner.query(\`${upQuery.query.replace(/`/g, '\\`')}\`${queryParams(upQuery.parameters)});`); 84 | }); 85 | 86 | sqlInMemory.downQueries.forEach((downQuery) => { 87 | down.push(`await queryRunner.query(\`${downQuery.query.replace(/`/g, '\\`')}\`${queryParams(downQuery.parameters)});`); 88 | }); 89 | 90 | await dataSource.destroy(); 91 | 92 | if ( 93 | up.length === 0 && 94 | down.length === 0 95 | ) { 96 | return { up, down }; 97 | } 98 | 99 | const content = buildTemplate(context.name, timestamp, up, down.reverse()); 100 | 101 | if (!context.preview) { 102 | let directoryPath : string; 103 | if (context.directoryPath) { 104 | if (!path.isAbsolute(context.directoryPath)) { 105 | directoryPath = path.join(process.cwd(), context.directoryPath); 106 | } else { 107 | directoryPath = context.directoryPath; 108 | } 109 | } else { 110 | directoryPath = path.join(process.cwd(), 'migrations'); 111 | } 112 | 113 | try { 114 | await fs.promises.access(directoryPath, fs.constants.R_OK | fs.constants.W_OK); 115 | } catch (e) { 116 | await fs.promises.mkdir(directoryPath, { recursive: true }); 117 | } 118 | 119 | const filePath = path.join(directoryPath, fileName); 120 | 121 | await fs.promises.writeFile(filePath, content, { encoding: 'utf-8' }); 122 | } 123 | 124 | return { 125 | up, 126 | down, 127 | content, 128 | }; 129 | } 130 | -------------------------------------------------------------------------------- /src/helpers/entity/uniqueness.ts: -------------------------------------------------------------------------------- 1 | import { FilterComparisonOperator } from 'rapiq'; 2 | import type { FiltersParseOutputElement } from 'rapiq'; 3 | import { Brackets } from 'typeorm'; 4 | import type { 5 | DataSource, EntityTarget, ObjectLiteral, WhereExpressionBuilder, 6 | } from 'typeorm'; 7 | import { useDataSource } from '../../data-source'; 8 | import { applyFiltersTransformed, transformParsedFilters } from '../../query'; 9 | import { pickRecord } from '../../utils'; 10 | import { getEntityMetadata } from './metadata'; 11 | 12 | type EntityUniquenessCheckOptions = { 13 | /** 14 | * Repository entity class. 15 | */ 16 | entityTarget: EntityTarget, 17 | 18 | /** 19 | * Entity to insert/update. 20 | */ 21 | entity: Partial, 22 | 23 | /** 24 | * Entity found. 25 | */ 26 | entityExisting?: Partial | null, 27 | 28 | /** 29 | * DataSource to use 30 | */ 31 | dataSource?: DataSource 32 | }; 33 | 34 | function transformUndefinedToNull(input: undefined | T) : T { 35 | if (typeof input === 'undefined') { 36 | return null as T; 37 | } 38 | 39 | return input; 40 | } 41 | 42 | function applyWhereExpression( 43 | qb: WhereExpressionBuilder, 44 | data: Record, 45 | type: 'source' | 'target', 46 | ) { 47 | const elements : FiltersParseOutputElement[] = []; 48 | 49 | const keys = Object.keys(data); 50 | for (let i = 0; i < keys.length; i++) { 51 | elements.push({ 52 | key: keys[i], 53 | value: transformUndefinedToNull(data[keys[i]]), 54 | operator: type === 'target' ? 55 | FilterComparisonOperator.EQUAL : 56 | FilterComparisonOperator.NOT_EQUAL, 57 | }); 58 | } 59 | 60 | const queryFilters = transformParsedFilters(elements, { 61 | bindingKey(key) { 62 | if (type === 'source') { 63 | return `filter_source_${key}`; 64 | } 65 | 66 | return `filter_target_${key}`; 67 | }, 68 | }); 69 | 70 | applyFiltersTransformed(qb, queryFilters); 71 | 72 | return queryFilters; 73 | } 74 | 75 | /** 76 | * Check if a given entity does not already exist. 77 | * Composite unique keys on a null column can only be present once. 78 | * 79 | * @experimental 80 | * @param options 81 | */ 82 | export async function isEntityUnique( 83 | options: EntityUniquenessCheckOptions, 84 | ) : Promise { 85 | const dataSource = options.dataSource || await useDataSource(); 86 | 87 | const metadata = await getEntityMetadata(options.entityTarget, dataSource); 88 | 89 | const repository = dataSource.getRepository(metadata.target); 90 | 91 | const primaryColumnNames = metadata.primaryColumns.map((c) => c.propertyName); 92 | 93 | const columnGroups : string[][] = []; 94 | if ( 95 | metadata.ownUniques && 96 | metadata.ownUniques.length > 0 97 | ) { 98 | for (let i = 0; i < metadata.ownUniques.length; i++) { 99 | columnGroups.push(metadata.ownUniques[i].columns.map( 100 | (column) => column.propertyName, 101 | )); 102 | } 103 | } else { 104 | for (let i = 0; i < metadata.indices.length; i++) { 105 | const index = metadata.indices[i]; 106 | if (!index.isUnique || index.entityMetadata.target !== metadata.target) { 107 | continue; 108 | } 109 | 110 | columnGroups.push(index.columns.map( 111 | (column) => column.propertyName, 112 | )); 113 | } 114 | } 115 | 116 | for (let i = 0; i < columnGroups.length; i++) { 117 | const queryBuilder = repository.createQueryBuilder('entity'); 118 | queryBuilder.where(new Brackets((qb) => { 119 | applyWhereExpression(qb, pickRecord(options.entity, columnGroups[i]), 'target'); 120 | })); 121 | 122 | if (options.entityExisting) { 123 | queryBuilder.andWhere(new Brackets((qb) => { 124 | applyWhereExpression(qb, pickRecord(options.entityExisting!, primaryColumnNames), 'source'); 125 | })); 126 | } 127 | 128 | const entity = await queryBuilder.getOne(); 129 | if (entity) { 130 | return false; 131 | } 132 | } 133 | 134 | return true; 135 | } 136 | --------------------------------------------------------------------------------