mirror of
https://github.com/typeorm/typeorm.git
synced 2025-12-08 21:26:23 +00:00
feat: add new foreign key decorator, and entity schemas options (#11144)
* feat: add new foreign key decorator, and entity schemas options This new feature adds the ability to create foreign key constraints without entity relations, using `@ForeignKey()` decorator or entity schema options. Closes: #4569
This commit is contained in:
parent
055eafd2e4
commit
6ebae3b795
@ -41,6 +41,7 @@
|
||||
- [`@Unique`](#unique)
|
||||
- [`@Check`](#check)
|
||||
- [`@Exclusion`](#exclusion)
|
||||
- [`@ForeignKey`](#foreignkey)
|
||||
|
||||
## Entity decorators
|
||||
|
||||
@ -963,3 +964,88 @@ export class RoomBooking {
|
||||
```
|
||||
|
||||
> Note: Only PostgreSQL supports exclusion constraints.
|
||||
|
||||
#### `@ForeignKey`
|
||||
|
||||
This decorator allows you to create a database foreign key for a specific column or columns.
|
||||
This decorator can be applied to columns or an entity itself.
|
||||
Use it on a column when an foreign key on a single column is needed
|
||||
and use it on the entity when a single foreign key on multiple columns is required.
|
||||
|
||||
> Note: **Do not use this decorator with relations.** Foreign keys are created automatically for relations
|
||||
> which you define using [Relation decorators](#relation-decorators) (`@ManyToOne`, `@OneToOne`, etc).
|
||||
> The `@ForeignKey` decorator should only be used to create foreign keys in the database when you
|
||||
> don't want to define an equivalent entity relationship.
|
||||
|
||||
Examples:
|
||||
|
||||
```typescript
|
||||
@Entity("orders")
|
||||
@ForeignKey(() => City, ["cityId", "countryCode"], ["id", "countryCode"])
|
||||
export class Order {
|
||||
@PrimaryColumn()
|
||||
id: number
|
||||
|
||||
@Column("uuid", { name: "user_uuid" })
|
||||
@ForeignKey<User>("User", "uuid", { name: "FK_user_uuid" })
|
||||
userUuid: string
|
||||
|
||||
@Column({ length: 2 })
|
||||
@ForeignKey(() => Country, "code")
|
||||
countryCode: string
|
||||
|
||||
@Column()
|
||||
@ForeignKey("cities")
|
||||
cityId: number
|
||||
|
||||
@Column()
|
||||
dispatchCountryCode: string
|
||||
|
||||
@ManyToOne(() => Country)
|
||||
dispatchCountry: Country
|
||||
|
||||
@Column()
|
||||
dispatchCityId: number
|
||||
|
||||
@ManyToOne(() => City)
|
||||
dispatchCity: City
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
@Entity("cities")
|
||||
@Unique(["id", "countryCode"])
|
||||
export class City {
|
||||
@PrimaryColumn()
|
||||
id: number
|
||||
|
||||
@Column({ length: 2 })
|
||||
@ForeignKey("countries", { onDelete: "CASCADE", onUpdate: "CASCADE" })
|
||||
countryCode: string
|
||||
|
||||
@Column()
|
||||
name: string
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
@Entity("countries")
|
||||
export class Country {
|
||||
@PrimaryColumn({ length: 2 })
|
||||
code: string
|
||||
|
||||
@Column()
|
||||
name: string
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
@Entity("users")
|
||||
export class User {
|
||||
@PrimaryColumn({ name: "ref" })
|
||||
id: number
|
||||
|
||||
@Column("uuid", { unique: true })
|
||||
uuid: string
|
||||
}
|
||||
```
|
||||
|
||||
@ -85,6 +85,20 @@ export const PersonSchema = new EntitySchema({
|
||||
type: Number,
|
||||
nullable: false,
|
||||
},
|
||||
countryCode: {
|
||||
type: String,
|
||||
length: 2,
|
||||
foreignKey: {
|
||||
target: "countries", // CountryEntity
|
||||
inverseSide: "code",
|
||||
},
|
||||
},
|
||||
cityId: {
|
||||
type: Number,
|
||||
foreignKey: {
|
||||
target: "cities", // CityEntity
|
||||
},
|
||||
},
|
||||
},
|
||||
checks: [
|
||||
{ expression: `"firstName" <> 'John' AND "lastName" <> 'Doe'` },
|
||||
@ -103,6 +117,13 @@ export const PersonSchema = new EntitySchema({
|
||||
columns: ["firstName", "lastName"],
|
||||
},
|
||||
],
|
||||
foreignKeys: [
|
||||
{
|
||||
target: "cities", // CityEntity
|
||||
columnNames: ["cityId", "countryCode"],
|
||||
referencedColumnNames: ["id", "countryCode"],
|
||||
},
|
||||
],
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@ -267,6 +267,11 @@ exports.Tree = Tree;
|
||||
}
|
||||
exports.Index = Index;
|
||||
|
||||
/* export */ function ForeignKey() {
|
||||
return noop
|
||||
}
|
||||
exports.ForeignKey = ForeignKey;
|
||||
|
||||
/* export */ function Unique() {
|
||||
return noop
|
||||
}
|
||||
@ -295,4 +300,4 @@ exports.EntityRepository = EntityRepository;
|
||||
/* export */ function VirtualColumn() {
|
||||
return noop
|
||||
}
|
||||
exports.VirtualColumn = VirtualColumn;
|
||||
exports.VirtualColumn = VirtualColumn;
|
||||
|
||||
91
sample/sample35-foreign-keys/app.ts
Normal file
91
sample/sample35-foreign-keys/app.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import { DataSource, DataSourceOptions } from "../../src"
|
||||
import { City } from "./entity/city"
|
||||
import { Country } from "./entity/country"
|
||||
import { Order } from "./entity/order"
|
||||
import { User } from "./entity/user"
|
||||
|
||||
const options: DataSourceOptions = {
|
||||
type: "postgres",
|
||||
host: "localhost",
|
||||
port: 5432,
|
||||
username: "test",
|
||||
password: "test",
|
||||
database: "test",
|
||||
logging: false,
|
||||
synchronize: true,
|
||||
entities: [City, Country, Order, User],
|
||||
}
|
||||
|
||||
const dataSource = new DataSource(options)
|
||||
dataSource.initialize().then(
|
||||
async (dataSource) => {
|
||||
await dataSource.getRepository(Country).save([
|
||||
{ code: "US", name: "United States" },
|
||||
{ code: "UA", name: "Ukraine" },
|
||||
])
|
||||
|
||||
await dataSource.getRepository(City).save([
|
||||
{ id: 1, name: "New York", countryCode: "US" },
|
||||
{ id: 2, name: "Kiev", countryCode: "UA" },
|
||||
])
|
||||
|
||||
await dataSource.getRepository(User).save([
|
||||
{
|
||||
id: 1,
|
||||
name: "Alice",
|
||||
uuid: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Bob",
|
||||
uuid: "c9bf9e57-1685-4c89-bafb-ff5af830be8a",
|
||||
},
|
||||
])
|
||||
|
||||
await dataSource.getRepository(Order).save([
|
||||
{
|
||||
id: 1,
|
||||
userUuid: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
||||
cityId: 1,
|
||||
countryCode: "US",
|
||||
dispatchCityId: 1,
|
||||
dispatchCountryCode: "US",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
userUuid: "c9bf9e57-1685-4c89-bafb-ff5af830be8a",
|
||||
cityId: 2,
|
||||
countryCode: "UA",
|
||||
dispatchCityId: 1,
|
||||
dispatchCountryCode: "US",
|
||||
},
|
||||
])
|
||||
|
||||
const ordersViaQueryBuilder = await dataSource
|
||||
.createQueryBuilder(Order, "orders")
|
||||
.leftJoinAndSelect(User, "users", "users.uuid = orders.userUuid")
|
||||
.leftJoinAndSelect(
|
||||
Country,
|
||||
"country",
|
||||
"country.code = orders.countryCode",
|
||||
)
|
||||
.leftJoinAndSelect("cities", "city", "city.id = orders.cityId")
|
||||
.leftJoinAndSelect("orders.dispatchCountry", "dispatchCountry")
|
||||
.leftJoinAndSelect("orders.dispatchCity", "dispatchCity")
|
||||
.orderBy("orders.id", "ASC")
|
||||
.getRawMany()
|
||||
|
||||
console.log(ordersViaQueryBuilder)
|
||||
|
||||
const ordersViaFind = await dataSource.getRepository(Order).find({
|
||||
relations: {
|
||||
dispatchCountry: true,
|
||||
dispatchCity: true,
|
||||
},
|
||||
order: { id: "asc" },
|
||||
})
|
||||
|
||||
console.log(ordersViaFind)
|
||||
},
|
||||
(error) => console.log("Cannot connect: ", error),
|
||||
)
|
||||
15
sample/sample35-foreign-keys/entity/city.ts
Normal file
15
sample/sample35-foreign-keys/entity/city.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { Column, Entity, ForeignKey, PrimaryColumn, Unique } from "../../../src"
|
||||
|
||||
@Entity("cities")
|
||||
@Unique(["id", "countryCode"])
|
||||
export class City {
|
||||
@PrimaryColumn()
|
||||
id: number
|
||||
|
||||
@Column({ length: 2 })
|
||||
@ForeignKey("countries", { onDelete: "CASCADE", onUpdate: "CASCADE" })
|
||||
countryCode: string
|
||||
|
||||
@Column()
|
||||
name: string
|
||||
}
|
||||
10
sample/sample35-foreign-keys/entity/country.ts
Normal file
10
sample/sample35-foreign-keys/entity/country.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Column, Entity, PrimaryColumn } from "../../../src"
|
||||
|
||||
@Entity("countries")
|
||||
export class Country {
|
||||
@PrimaryColumn({ length: 2 })
|
||||
code: string
|
||||
|
||||
@Column()
|
||||
name: string
|
||||
}
|
||||
42
sample/sample35-foreign-keys/entity/order.ts
Normal file
42
sample/sample35-foreign-keys/entity/order.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
ForeignKey,
|
||||
ManyToOne,
|
||||
PrimaryColumn,
|
||||
} from "../../../src"
|
||||
import { City } from "./city"
|
||||
import { Country } from "./country"
|
||||
|
||||
import { User } from "./user"
|
||||
|
||||
@Entity("orders")
|
||||
@ForeignKey(() => City, ["cityId", "countryCode"], ["id", "countryCode"])
|
||||
export class Order {
|
||||
@PrimaryColumn()
|
||||
id: number
|
||||
|
||||
@Column("uuid", { name: "user_uuid" })
|
||||
@ForeignKey<User>("User", "uuid", { name: "FK_user_uuid" })
|
||||
userUuid: string
|
||||
|
||||
@Column({ length: 2 })
|
||||
@ForeignKey(() => Country, "code")
|
||||
countryCode: string
|
||||
|
||||
@Column()
|
||||
@ForeignKey("cities")
|
||||
cityId: number
|
||||
|
||||
@Column({ length: 2 })
|
||||
dispatchCountryCode: string
|
||||
|
||||
@ManyToOne(() => Country)
|
||||
dispatchCountry: Country
|
||||
|
||||
@Column()
|
||||
dispatchCityId: number
|
||||
|
||||
@ManyToOne(() => City)
|
||||
dispatchCity: City
|
||||
}
|
||||
10
sample/sample35-foreign-keys/entity/user.ts
Normal file
10
sample/sample35-foreign-keys/entity/user.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Column, Entity, PrimaryColumn } from "../../../src"
|
||||
|
||||
@Entity("users")
|
||||
export class User {
|
||||
@PrimaryColumn({ name: "ref" })
|
||||
id: number
|
||||
|
||||
@Column("uuid", { unique: true })
|
||||
uuid: string
|
||||
}
|
||||
91
sample/sample36-schemas-foreign-keys/app.ts
Normal file
91
sample/sample36-schemas-foreign-keys/app.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import { DataSource, DataSourceOptions } from "../../src"
|
||||
import { CityEntity } from "./entity/city"
|
||||
import { CountryEntity } from "./entity/country"
|
||||
import { OrderEntity } from "./entity/order"
|
||||
import { UserEntity } from "./entity/user"
|
||||
|
||||
const options: DataSourceOptions = {
|
||||
type: "postgres",
|
||||
host: "localhost",
|
||||
port: 5432,
|
||||
username: "test",
|
||||
password: "test",
|
||||
database: "test",
|
||||
logging: false,
|
||||
synchronize: true,
|
||||
entities: [CityEntity, CountryEntity, OrderEntity, UserEntity],
|
||||
}
|
||||
|
||||
const dataSource = new DataSource(options)
|
||||
dataSource.initialize().then(
|
||||
async (dataSource) => {
|
||||
await dataSource.getRepository(CountryEntity).save([
|
||||
{ code: "US", name: "United States" },
|
||||
{ code: "UA", name: "Ukraine" },
|
||||
])
|
||||
|
||||
await dataSource.getRepository(CityEntity).save([
|
||||
{ id: 1, name: "New York", countryCode: "US" },
|
||||
{ id: 2, name: "Kiev", countryCode: "UA" },
|
||||
])
|
||||
|
||||
await dataSource.getRepository(UserEntity).save([
|
||||
{
|
||||
id: 1,
|
||||
name: "Alice",
|
||||
uuid: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Bob",
|
||||
uuid: "c9bf9e57-1685-4c89-bafb-ff5af830be8a",
|
||||
},
|
||||
])
|
||||
|
||||
await dataSource.getRepository(OrderEntity).save([
|
||||
{
|
||||
id: 1,
|
||||
userUuid: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
||||
cityId: 1,
|
||||
countryCode: "US",
|
||||
dispatchCityId: 1,
|
||||
dispatchCountryCode: "US",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
userUuid: "c9bf9e57-1685-4c89-bafb-ff5af830be8a",
|
||||
cityId: 2,
|
||||
countryCode: "UA",
|
||||
dispatchCityId: 1,
|
||||
dispatchCountryCode: "US",
|
||||
},
|
||||
])
|
||||
|
||||
const ordersViaQueryBuilder = await dataSource
|
||||
.createQueryBuilder(OrderEntity, "orders")
|
||||
.leftJoinAndSelect("User", "users", "users.uuid = orders.userUuid")
|
||||
.leftJoinAndSelect(
|
||||
"Country",
|
||||
"country",
|
||||
"country.code = orders.countryCode",
|
||||
)
|
||||
.leftJoinAndSelect("cities", "city", "city.id = orders.cityId")
|
||||
.leftJoinAndSelect("orders.dispatchCountry", "dispatchCountry")
|
||||
.leftJoinAndSelect("orders.dispatchCity", "dispatchCity")
|
||||
.orderBy("orders.id", "ASC")
|
||||
.getRawMany()
|
||||
|
||||
console.log(ordersViaQueryBuilder)
|
||||
|
||||
const ordersViaFind = await dataSource.getRepository(OrderEntity).find({
|
||||
relations: {
|
||||
dispatchCountry: true,
|
||||
dispatchCity: true,
|
||||
},
|
||||
order: { id: "asc" },
|
||||
})
|
||||
|
||||
console.log(ordersViaFind)
|
||||
},
|
||||
(error) => console.log("Cannot connect: ", error),
|
||||
)
|
||||
32
sample/sample36-schemas-foreign-keys/entity/city.ts
Normal file
32
sample/sample36-schemas-foreign-keys/entity/city.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { EntitySchema } from "../../../src"
|
||||
import { City } from "../model/city"
|
||||
|
||||
import { CountryEntity } from "./country"
|
||||
|
||||
export const CityEntity = new EntitySchema<City>({
|
||||
name: "City",
|
||||
tableName: "cities",
|
||||
columns: {
|
||||
id: {
|
||||
primary: true,
|
||||
type: Number,
|
||||
},
|
||||
countryCode: {
|
||||
type: String,
|
||||
length: 2,
|
||||
foreignKey: {
|
||||
target: CountryEntity,
|
||||
onDelete: "CASCADE",
|
||||
onUpdate: "CASCADE",
|
||||
},
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
uniques: [
|
||||
{
|
||||
columns: ["id", "countryCode"],
|
||||
},
|
||||
],
|
||||
})
|
||||
17
sample/sample36-schemas-foreign-keys/entity/country.ts
Normal file
17
sample/sample36-schemas-foreign-keys/entity/country.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { EntitySchema } from "../../../src"
|
||||
import { Country } from "../model/country"
|
||||
|
||||
export const CountryEntity = new EntitySchema<Country>({
|
||||
name: "Country",
|
||||
tableName: "countries",
|
||||
columns: {
|
||||
code: {
|
||||
primary: true,
|
||||
type: String,
|
||||
length: 2,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
})
|
||||
68
sample/sample36-schemas-foreign-keys/entity/order.ts
Normal file
68
sample/sample36-schemas-foreign-keys/entity/order.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { EntitySchema } from "../../../src"
|
||||
import { Order } from "../model/order"
|
||||
import { CityEntity } from "./city"
|
||||
import { CountryEntity } from "./country"
|
||||
|
||||
export const OrderEntity = new EntitySchema<Order>({
|
||||
name: "Order",
|
||||
tableName: "orders",
|
||||
columns: {
|
||||
id: {
|
||||
primary: true,
|
||||
type: Number,
|
||||
},
|
||||
userUuid: {
|
||||
type: "uuid",
|
||||
name: "user_uuid",
|
||||
foreignKey: {
|
||||
target: "User",
|
||||
inverseSide: "uuid",
|
||||
name: "FK_user_uuid",
|
||||
},
|
||||
},
|
||||
countryCode: {
|
||||
type: String,
|
||||
length: 2,
|
||||
foreignKey: {
|
||||
target: CountryEntity,
|
||||
inverseSide: "code",
|
||||
},
|
||||
},
|
||||
cityId: {
|
||||
type: Number,
|
||||
foreignKey: {
|
||||
target: "cities",
|
||||
},
|
||||
},
|
||||
dispatchCountryCode: {
|
||||
type: String,
|
||||
length: 2,
|
||||
},
|
||||
dispatchCityId: {
|
||||
type: Number,
|
||||
},
|
||||
},
|
||||
relations: {
|
||||
dispatchCountry: {
|
||||
type: "many-to-one",
|
||||
target: () => "Country",
|
||||
joinColumn: {
|
||||
name: "dispatchCountryCode",
|
||||
},
|
||||
},
|
||||
dispatchCity: {
|
||||
type: "many-to-one",
|
||||
target: CityEntity,
|
||||
joinColumn: {
|
||||
name: "dispatchCityId",
|
||||
},
|
||||
},
|
||||
},
|
||||
foreignKeys: [
|
||||
{
|
||||
target: CityEntity,
|
||||
columnNames: ["cityId", "countryCode"],
|
||||
referencedColumnNames: ["id", "countryCode"],
|
||||
},
|
||||
],
|
||||
})
|
||||
18
sample/sample36-schemas-foreign-keys/entity/user.ts
Normal file
18
sample/sample36-schemas-foreign-keys/entity/user.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { EntitySchema } from "../../../src"
|
||||
import { User } from "../model/user"
|
||||
|
||||
export const UserEntity = new EntitySchema<User>({
|
||||
name: "User",
|
||||
tableName: "users",
|
||||
columns: {
|
||||
id: {
|
||||
primary: true,
|
||||
type: Number,
|
||||
name: "ref",
|
||||
},
|
||||
uuid: {
|
||||
type: "uuid",
|
||||
unique: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
5
sample/sample36-schemas-foreign-keys/model/city.ts
Normal file
5
sample/sample36-schemas-foreign-keys/model/city.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export class City {
|
||||
id: number
|
||||
countryCode: string
|
||||
name: string
|
||||
}
|
||||
4
sample/sample36-schemas-foreign-keys/model/country.ts
Normal file
4
sample/sample36-schemas-foreign-keys/model/country.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export class Country {
|
||||
code: string
|
||||
name: string
|
||||
}
|
||||
13
sample/sample36-schemas-foreign-keys/model/order.ts
Normal file
13
sample/sample36-schemas-foreign-keys/model/order.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { City } from "./city"
|
||||
import { Country } from "./country"
|
||||
|
||||
export class Order {
|
||||
id: number
|
||||
userUuid: string
|
||||
countryCode: string
|
||||
cityId: number
|
||||
dispatchCountryCode: string
|
||||
dispatchCountry: Country
|
||||
dispatchCityId: number
|
||||
dispatchCity: City
|
||||
}
|
||||
4
sample/sample36-schemas-foreign-keys/model/user.ts
Normal file
4
sample/sample36-schemas-foreign-keys/model/user.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export class User {
|
||||
id: number
|
||||
uuid: string
|
||||
}
|
||||
104
src/decorator/ForeignKey.ts
Normal file
104
src/decorator/ForeignKey.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { ObjectType } from "../common/ObjectType"
|
||||
import { getMetadataArgsStorage } from "../globals"
|
||||
import { ForeignKeyMetadataArgs } from "../metadata-args/ForeignKeyMetadataArgs"
|
||||
import { ObjectUtils } from "../util/ObjectUtils"
|
||||
import { ForeignKeyOptions } from "./options/ForeignKeyOptions"
|
||||
|
||||
/**
|
||||
* Creates a database foreign key. Can be used on entity property or on entity.
|
||||
* Can create foreign key with composite columns when used on entity.
|
||||
* Warning! Don't use this with relations; relation decorators create foreign keys automatically.
|
||||
*/
|
||||
export function ForeignKey<T>(
|
||||
typeFunctionOrTarget: string | ((type?: any) => ObjectType<T>),
|
||||
options?: ForeignKeyOptions,
|
||||
): PropertyDecorator
|
||||
|
||||
/**
|
||||
* Creates a database foreign key. Can be used on entity property or on entity.
|
||||
* Can create foreign key with composite columns when used on entity.
|
||||
* Warning! Don't use this with relations; relation decorators create foreign keys automatically.
|
||||
*/
|
||||
export function ForeignKey<T>(
|
||||
typeFunctionOrTarget: string | ((type?: any) => ObjectType<T>),
|
||||
inverseSide: string | ((object: T) => any),
|
||||
options?: ForeignKeyOptions,
|
||||
): PropertyDecorator
|
||||
|
||||
/**
|
||||
* Creates a database foreign key. Can be used on entity property or on entity.
|
||||
* Can create foreign key with composite columns when used on entity.
|
||||
* Warning! Don't use this with relations; relation decorators create foreign keys automatically.
|
||||
*/
|
||||
export function ForeignKey<
|
||||
T,
|
||||
C extends (readonly [] | readonly string[]) &
|
||||
(number extends C["length"] ? readonly [] : unknown),
|
||||
>(
|
||||
typeFunctionOrTarget: string | ((type?: any) => ObjectType<T>),
|
||||
columnNames: C,
|
||||
referencedColumnNames: { [K in keyof C]: string },
|
||||
options?: ForeignKeyOptions,
|
||||
): ClassDecorator
|
||||
|
||||
/**
|
||||
* Creates a database foreign key. Can be used on entity property or on entity.
|
||||
* Can create foreign key with composite columns when used on entity.
|
||||
* Warning! Don't use this with relations; relation decorators create foreign keys automatically.
|
||||
*/
|
||||
export function ForeignKey<
|
||||
T,
|
||||
C extends (readonly [] | readonly string[]) &
|
||||
(number extends C["length"] ? readonly [] : unknown),
|
||||
>(
|
||||
typeFunctionOrTarget: string | ((type?: any) => ObjectType<T>),
|
||||
inverseSideOrColumnNamesOrOptions?:
|
||||
| string
|
||||
| ((object: T) => any)
|
||||
| C
|
||||
| ForeignKeyOptions,
|
||||
referencedColumnNamesOrOptions?:
|
||||
| { [K in keyof C]: string }
|
||||
| ForeignKeyOptions,
|
||||
maybeOptions?: ForeignKeyOptions,
|
||||
): ClassDecorator & PropertyDecorator {
|
||||
const inverseSide =
|
||||
typeof inverseSideOrColumnNamesOrOptions === "string" ||
|
||||
typeof inverseSideOrColumnNamesOrOptions === "function"
|
||||
? inverseSideOrColumnNamesOrOptions
|
||||
: undefined
|
||||
|
||||
const columnNames = Array.isArray(inverseSideOrColumnNamesOrOptions)
|
||||
? inverseSideOrColumnNamesOrOptions
|
||||
: undefined
|
||||
|
||||
const referencedColumnNames = Array.isArray(referencedColumnNamesOrOptions)
|
||||
? referencedColumnNamesOrOptions
|
||||
: undefined
|
||||
|
||||
const options =
|
||||
ObjectUtils.isObject(inverseSideOrColumnNamesOrOptions) &&
|
||||
!Array.isArray(inverseSideOrColumnNamesOrOptions)
|
||||
? inverseSideOrColumnNamesOrOptions
|
||||
: ObjectUtils.isObject(referencedColumnNamesOrOptions) &&
|
||||
!Array.isArray(referencedColumnNamesOrOptions)
|
||||
? referencedColumnNamesOrOptions
|
||||
: maybeOptions
|
||||
|
||||
return function (
|
||||
clsOrObject: Function | Object,
|
||||
propertyName?: string | symbol,
|
||||
) {
|
||||
getMetadataArgsStorage().foreignKeys.push({
|
||||
target: propertyName
|
||||
? clsOrObject.constructor
|
||||
: (clsOrObject as Function),
|
||||
propertyName: propertyName,
|
||||
type: typeFunctionOrTarget,
|
||||
inverseSide,
|
||||
columnNames,
|
||||
referencedColumnNames,
|
||||
...(options as ForeignKeyOptions),
|
||||
} as ForeignKeyMetadataArgs)
|
||||
}
|
||||
}
|
||||
28
src/decorator/options/ForeignKeyOptions.ts
Normal file
28
src/decorator/options/ForeignKeyOptions.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { DeferrableType } from "../../metadata/types/DeferrableType"
|
||||
import { OnDeleteType } from "../../metadata/types/OnDeleteType"
|
||||
import { OnUpdateType } from "../../metadata/types/OnUpdateType"
|
||||
|
||||
/**
|
||||
* Describes all foreign key options.
|
||||
*/
|
||||
export interface ForeignKeyOptions {
|
||||
/**
|
||||
* Name of the foreign key constraint.
|
||||
*/
|
||||
name?: string
|
||||
|
||||
/**
|
||||
* Database cascade action on delete.
|
||||
*/
|
||||
onDelete?: OnDeleteType
|
||||
|
||||
/**
|
||||
* Database cascade action on update.
|
||||
*/
|
||||
onUpdate?: OnUpdateType
|
||||
|
||||
/**
|
||||
* Indicate if foreign key constraints can be deferred.
|
||||
*/
|
||||
deferrable?: DeferrableType
|
||||
}
|
||||
14
src/entity-schema/EntitySchemaColumnForeignKeyOptions.ts
Normal file
14
src/entity-schema/EntitySchemaColumnForeignKeyOptions.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { EntityTarget } from "../common/EntityTarget"
|
||||
import { ForeignKeyOptions } from "../decorator/options/ForeignKeyOptions"
|
||||
|
||||
export interface EntitySchemaColumnForeignKeyOptions extends ForeignKeyOptions {
|
||||
/**
|
||||
* Indicates with which entity this relation is made.
|
||||
*/
|
||||
target: EntityTarget<any>
|
||||
|
||||
/**
|
||||
* Inverse side of the relation.
|
||||
*/
|
||||
inverseSide?: string
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import { ColumnType } from "../driver/types/ColumnTypes"
|
||||
import { ValueTransformer } from "../decorator/options/ValueTransformer"
|
||||
import { SpatialColumnOptions } from "../decorator/options/SpatialColumnOptions"
|
||||
import { EntitySchemaColumnForeignKeyOptions } from "./EntitySchemaColumnForeignKeyOptions"
|
||||
|
||||
export interface EntitySchemaColumnOptions extends SpatialColumnOptions {
|
||||
/**
|
||||
@ -208,4 +209,9 @@ export interface EntitySchemaColumnOptions extends SpatialColumnOptions {
|
||||
* Name of the primary key constraint.
|
||||
*/
|
||||
primaryKeyConstraintName?: string
|
||||
|
||||
/**
|
||||
* Foreign key options of this column.
|
||||
*/
|
||||
foreignKey?: EntitySchemaColumnForeignKeyOptions
|
||||
}
|
||||
|
||||
19
src/entity-schema/EntitySchemaForeignKeyOptions.ts
Normal file
19
src/entity-schema/EntitySchemaForeignKeyOptions.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { EntityTarget } from "../common/EntityTarget"
|
||||
import { ForeignKeyOptions } from "../decorator/options/ForeignKeyOptions"
|
||||
|
||||
export interface EntitySchemaForeignKeyOptions extends ForeignKeyOptions {
|
||||
/**
|
||||
* Indicates with which entity this relation is made.
|
||||
*/
|
||||
target: EntityTarget<any>
|
||||
|
||||
/**
|
||||
* Column names which included by this foreign key.
|
||||
*/
|
||||
columnNames: string[]
|
||||
|
||||
/**
|
||||
* Column names which included by this foreign key.
|
||||
*/
|
||||
referencedColumnNames: string[]
|
||||
}
|
||||
@ -13,6 +13,7 @@ import { EntitySchemaCheckOptions } from "./EntitySchemaCheckOptions"
|
||||
import { EntitySchemaExclusionOptions } from "./EntitySchemaExclusionOptions"
|
||||
import { EntitySchemaInheritanceOptions } from "./EntitySchemaInheritanceOptions"
|
||||
import { EntitySchemaRelationIdOptions } from "./EntitySchemaRelationIdOptions"
|
||||
import { EntitySchemaForeignKeyOptions } from "./EntitySchemaForeignKeyOptions"
|
||||
|
||||
/**
|
||||
* Interface for entity metadata mappings stored inside "schemas" instead of models decorated by decorators.
|
||||
@ -79,6 +80,11 @@ export class EntitySchemaOptions<T> {
|
||||
*/
|
||||
indices?: EntitySchemaIndexOptions[]
|
||||
|
||||
/**
|
||||
* Entity foreign keys options.
|
||||
*/
|
||||
foreignKeys?: EntitySchemaForeignKeyOptions[]
|
||||
|
||||
/**
|
||||
* Entity uniques options.
|
||||
*/
|
||||
|
||||
@ -18,6 +18,7 @@ import { EntitySchemaOptions } from "./EntitySchemaOptions"
|
||||
import { EntitySchemaEmbeddedError } from "./EntitySchemaEmbeddedError"
|
||||
import { InheritanceMetadataArgs } from "../metadata-args/InheritanceMetadataArgs"
|
||||
import { RelationIdMetadataArgs } from "../metadata-args/RelationIdMetadataArgs"
|
||||
import { ForeignKeyMetadataArgs } from "../metadata-args/ForeignKeyMetadataArgs"
|
||||
|
||||
/**
|
||||
* Transforms entity schema into metadata args storage.
|
||||
@ -155,6 +156,22 @@ export class EntitySchemaTransformer {
|
||||
target: options.target || options.name,
|
||||
columns: [columnName],
|
||||
})
|
||||
|
||||
if (regularColumn.foreignKey) {
|
||||
const foreignKey = regularColumn.foreignKey
|
||||
|
||||
const foreignKeyArgs: ForeignKeyMetadataArgs = {
|
||||
target: options.target || options.name,
|
||||
type: foreignKey.target,
|
||||
propertyName: columnName,
|
||||
inverseSide: foreignKey.inverseSide,
|
||||
name: foreignKey.name,
|
||||
onDelete: foreignKey.onDelete,
|
||||
onUpdate: foreignKey.onUpdate,
|
||||
deferrable: foreignKey.deferrable,
|
||||
}
|
||||
metadataArgsStorage.foreignKeys.push(foreignKeyArgs)
|
||||
}
|
||||
})
|
||||
|
||||
// add relation metadata args from the schema
|
||||
@ -296,6 +313,22 @@ export class EntitySchemaTransformer {
|
||||
})
|
||||
}
|
||||
|
||||
if (options.foreignKeys) {
|
||||
options.foreignKeys.forEach((foreignKey) => {
|
||||
const foreignKeyArgs: ForeignKeyMetadataArgs = {
|
||||
target: options.target || options.name,
|
||||
type: foreignKey.target,
|
||||
columnNames: foreignKey.columnNames,
|
||||
referencedColumnNames: foreignKey.referencedColumnNames,
|
||||
name: foreignKey.name,
|
||||
onDelete: foreignKey.onDelete,
|
||||
onUpdate: foreignKey.onUpdate,
|
||||
deferrable: foreignKey.deferrable,
|
||||
}
|
||||
metadataArgsStorage.foreignKeys.push(foreignKeyArgs)
|
||||
})
|
||||
}
|
||||
|
||||
// add unique metadata args from the schema
|
||||
if (options.uniques) {
|
||||
options.uniques.forEach((unique) => {
|
||||
|
||||
@ -61,6 +61,7 @@ export * from "./decorator/tree/TreeParent"
|
||||
export * from "./decorator/tree/TreeChildren"
|
||||
export * from "./decorator/tree/Tree"
|
||||
export * from "./decorator/Index"
|
||||
export * from "./decorator/ForeignKey"
|
||||
export * from "./decorator/Unique"
|
||||
export * from "./decorator/Check"
|
||||
export * from "./decorator/Exclusion"
|
||||
|
||||
64
src/metadata-args/ForeignKeyMetadataArgs.ts
Normal file
64
src/metadata-args/ForeignKeyMetadataArgs.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { DeferrableType } from "../metadata/types/DeferrableType"
|
||||
import { OnDeleteType } from "../metadata/types/OnDeleteType"
|
||||
import { OnUpdateType } from "../metadata/types/OnUpdateType"
|
||||
import { PropertyTypeFactory } from "../metadata/types/PropertyTypeInFunction"
|
||||
import { RelationTypeInFunction } from "../metadata/types/RelationTypeInFunction"
|
||||
|
||||
/**
|
||||
* Arguments for ForeignKeyMetadata class.
|
||||
*/
|
||||
export interface ForeignKeyMetadataArgs {
|
||||
/**
|
||||
* Class to which foreign key is applied.
|
||||
*/
|
||||
readonly target: Function | string
|
||||
|
||||
/**
|
||||
* Class's property name to which this foreign key is applied.
|
||||
*/
|
||||
readonly propertyName?: string
|
||||
|
||||
/**
|
||||
* Type of the relation. This type is in function because of language specifics and problems with recursive
|
||||
* referenced classes.
|
||||
*/
|
||||
readonly type: RelationTypeInFunction
|
||||
|
||||
/**
|
||||
* Foreign key constraint name.
|
||||
*/
|
||||
readonly name?: string
|
||||
|
||||
/**
|
||||
* Inverse side of the relation.
|
||||
*/
|
||||
readonly inverseSide?: PropertyTypeFactory<any>
|
||||
|
||||
/**
|
||||
* Column names which included by this foreign key.
|
||||
*/
|
||||
readonly columnNames?: string[]
|
||||
|
||||
/**
|
||||
* Column names which included by this foreign key.
|
||||
*/
|
||||
readonly referencedColumnNames?: string[]
|
||||
|
||||
/**
|
||||
* "ON DELETE" of this foreign key, e.g. what action database should perform when
|
||||
* referenced stuff is being deleted.
|
||||
*/
|
||||
readonly onDelete?: OnDeleteType
|
||||
|
||||
/**
|
||||
* "ON UPDATE" of this foreign key, e.g. what action database should perform when
|
||||
* referenced stuff is being updated.
|
||||
*/
|
||||
readonly onUpdate?: OnUpdateType
|
||||
|
||||
/**
|
||||
* Set this foreign key constraint as "DEFERRABLE" e.g. check constraints at start
|
||||
* or at the end of a transaction
|
||||
*/
|
||||
readonly deferrable?: DeferrableType
|
||||
}
|
||||
@ -21,6 +21,7 @@ import { TreeMetadataArgs } from "./TreeMetadataArgs"
|
||||
import { UniqueMetadataArgs } from "./UniqueMetadataArgs"
|
||||
import { CheckMetadataArgs } from "./CheckMetadataArgs"
|
||||
import { ExclusionMetadataArgs } from "./ExclusionMetadataArgs"
|
||||
import { ForeignKeyMetadataArgs } from "./ForeignKeyMetadataArgs"
|
||||
|
||||
/**
|
||||
* Storage all metadatas args of all available types: tables, columns, subscribers, relations, etc.
|
||||
@ -40,6 +41,7 @@ export class MetadataArgsStorage {
|
||||
readonly namingStrategies: NamingStrategyMetadataArgs[] = []
|
||||
readonly entitySubscribers: EntitySubscriberMetadataArgs[] = []
|
||||
readonly indices: IndexMetadataArgs[] = []
|
||||
readonly foreignKeys: ForeignKeyMetadataArgs[] = []
|
||||
readonly uniques: UniqueMetadataArgs[] = []
|
||||
readonly checks: CheckMetadataArgs[] = []
|
||||
readonly exclusions: ExclusionMetadataArgs[] = []
|
||||
@ -158,6 +160,18 @@ export class MetadataArgsStorage {
|
||||
})
|
||||
}
|
||||
|
||||
filterForeignKeys(target: Function | string): ForeignKeyMetadataArgs[]
|
||||
filterForeignKeys(target: (Function | string)[]): ForeignKeyMetadataArgs[]
|
||||
filterForeignKeys(
|
||||
target: (Function | string) | (Function | string)[],
|
||||
): ForeignKeyMetadataArgs[] {
|
||||
return this.foreignKeys.filter((foreignKey) => {
|
||||
return Array.isArray(target)
|
||||
? target.indexOf(foreignKey.target) !== -1
|
||||
: foreignKey.target === target
|
||||
})
|
||||
}
|
||||
|
||||
filterUniques(target: Function | string): UniqueMetadataArgs[]
|
||||
filterUniques(target: (Function | string)[]): UniqueMetadataArgs[]
|
||||
filterUniques(
|
||||
|
||||
@ -20,6 +20,8 @@ import { CheckMetadata } from "../metadata/CheckMetadata"
|
||||
import { ExclusionMetadata } from "../metadata/ExclusionMetadata"
|
||||
import { TypeORMError } from "../error"
|
||||
import { DriverUtils } from "../driver/DriverUtils"
|
||||
import { ForeignKeyMetadata } from "../metadata/ForeignKeyMetadata"
|
||||
import { InstanceChecker } from "../util/InstanceChecker"
|
||||
|
||||
/**
|
||||
* Builds EntityMetadata objects and all its sub-metadatas.
|
||||
@ -386,6 +388,11 @@ export class EntityMetadataBuilder {
|
||||
)
|
||||
})
|
||||
|
||||
// generate foreign keys for tables
|
||||
entityMetadatas.forEach((entityMetadata) =>
|
||||
this.createForeignKeys(entityMetadata, entityMetadatas),
|
||||
)
|
||||
|
||||
// add lazy initializer for entity relations
|
||||
entityMetadatas
|
||||
.filter((metadata) => typeof metadata.target === "function")
|
||||
@ -1169,4 +1176,120 @@ export class EntityMetadataBuilder {
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates from the given foreign key metadata args real foreign key metadatas.
|
||||
*/
|
||||
protected createForeignKeys(
|
||||
entityMetadata: EntityMetadata,
|
||||
entityMetadatas: EntityMetadata[],
|
||||
) {
|
||||
this.metadataArgsStorage
|
||||
.filterForeignKeys(entityMetadata.inheritanceTree)
|
||||
.forEach((foreignKeyArgs) => {
|
||||
const foreignKeyType =
|
||||
typeof foreignKeyArgs.type === "function"
|
||||
? (foreignKeyArgs.type as () => any)()
|
||||
: foreignKeyArgs.type
|
||||
|
||||
const referencedEntityMetadata = entityMetadatas.find((m) =>
|
||||
typeof foreignKeyType === "string"
|
||||
? m.targetName === foreignKeyType ||
|
||||
m.givenTableName === foreignKeyType
|
||||
: InstanceChecker.isEntitySchema(foreignKeyType)
|
||||
? m.target === foreignKeyType.options.name ||
|
||||
m.target === foreignKeyType.options.target
|
||||
: m.target === foreignKeyType,
|
||||
)
|
||||
|
||||
if (!referencedEntityMetadata) {
|
||||
throw new TypeORMError(
|
||||
"Entity metadata for " +
|
||||
entityMetadata.name +
|
||||
(foreignKeyArgs.propertyName
|
||||
? "#" + foreignKeyArgs.propertyName
|
||||
: "") +
|
||||
" was not found. Check if you specified a correct entity object and if it's connected in the connection options.",
|
||||
)
|
||||
}
|
||||
|
||||
const columnNames = foreignKeyArgs.columnNames ?? []
|
||||
const referencedColumnNames =
|
||||
foreignKeyArgs.referencedColumnNames ?? []
|
||||
|
||||
const columns: ColumnMetadata[] = []
|
||||
const referencedColumns: ColumnMetadata[] = []
|
||||
|
||||
if (foreignKeyArgs.propertyName) {
|
||||
columnNames.push(foreignKeyArgs.propertyName)
|
||||
|
||||
if (foreignKeyArgs.inverseSide) {
|
||||
if (typeof foreignKeyArgs.inverseSide === "function") {
|
||||
referencedColumnNames.push(
|
||||
foreignKeyArgs.inverseSide(
|
||||
referencedEntityMetadata.propertiesMap,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
referencedColumnNames.push(
|
||||
foreignKeyArgs.inverseSide,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!referencedColumnNames.length) {
|
||||
referencedColumns.push(
|
||||
...referencedEntityMetadata.primaryColumns,
|
||||
)
|
||||
}
|
||||
|
||||
const columnNameToColumn = (
|
||||
columnName: string,
|
||||
entityMetadata: EntityMetadata,
|
||||
): ColumnMetadata => {
|
||||
const column = entityMetadata.columns.find(
|
||||
(column) =>
|
||||
column.propertyName === columnName ||
|
||||
column.databaseName === columnName,
|
||||
)
|
||||
|
||||
if (column) return column
|
||||
|
||||
const foreignKeyName = foreignKeyArgs.name
|
||||
? '"' + foreignKeyArgs.name + '" '
|
||||
: ""
|
||||
const entityName = entityMetadata.targetName
|
||||
throw new TypeORMError(
|
||||
`Foreign key constraint ${foreignKeyName}contains column that is missing in the entity (${entityName}): ${columnName}`,
|
||||
)
|
||||
}
|
||||
|
||||
columns.push(
|
||||
...columnNames.map((columnName) =>
|
||||
columnNameToColumn(columnName, entityMetadata),
|
||||
),
|
||||
)
|
||||
|
||||
referencedColumns.push(
|
||||
...referencedColumnNames.map((columnName) =>
|
||||
columnNameToColumn(
|
||||
columnName,
|
||||
referencedEntityMetadata,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
entityMetadata.foreignKeys.push(
|
||||
new ForeignKeyMetadata({
|
||||
entityMetadata,
|
||||
referencedEntityMetadata,
|
||||
namingStrategy: this.connection.namingStrategy,
|
||||
columns,
|
||||
referencedColumns,
|
||||
...foreignKeyArgs,
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
21
test/functional/decorators/foreign-key/entity/city.ts
Normal file
21
test/functional/decorators/foreign-key/entity/city.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
ForeignKey,
|
||||
PrimaryColumn,
|
||||
Unique,
|
||||
} from "../../../../../src"
|
||||
|
||||
@Entity("cities")
|
||||
@Unique(["id", "countryCode"])
|
||||
export class City {
|
||||
@PrimaryColumn()
|
||||
id: number
|
||||
|
||||
@Column({ length: 2 })
|
||||
@ForeignKey("countries", { onDelete: "CASCADE", onUpdate: "CASCADE" })
|
||||
countryCode: string
|
||||
|
||||
@Column()
|
||||
name: string
|
||||
}
|
||||
10
test/functional/decorators/foreign-key/entity/country.ts
Normal file
10
test/functional/decorators/foreign-key/entity/country.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Column, Entity, PrimaryColumn } from "../../../../../src"
|
||||
|
||||
@Entity("countries")
|
||||
export class Country {
|
||||
@PrimaryColumn({ length: 2 })
|
||||
code: string
|
||||
|
||||
@Column()
|
||||
name: string
|
||||
}
|
||||
42
test/functional/decorators/foreign-key/entity/order.ts
Normal file
42
test/functional/decorators/foreign-key/entity/order.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
ForeignKey,
|
||||
ManyToOne,
|
||||
PrimaryColumn,
|
||||
} from "../../../../../src"
|
||||
import { City } from "./city"
|
||||
import { Country } from "./country"
|
||||
|
||||
import { User } from "./user"
|
||||
|
||||
@Entity("orders")
|
||||
@ForeignKey(() => City, ["cityId", "countryCode"], ["id", "countryCode"])
|
||||
export class Order {
|
||||
@PrimaryColumn()
|
||||
id: number
|
||||
|
||||
@Column("uuid", { name: "user_uuid" })
|
||||
@ForeignKey<User>("User", "uuid", { name: "FK_user_uuid" })
|
||||
userUuid: string
|
||||
|
||||
@Column({ length: 2 })
|
||||
@ForeignKey(() => Country, (x) => x.code)
|
||||
countryCode: string
|
||||
|
||||
@Column()
|
||||
@ForeignKey("cities")
|
||||
cityId: number
|
||||
|
||||
@Column({ length: 2 })
|
||||
dispatchCountryCode: string
|
||||
|
||||
@ManyToOne(() => Country)
|
||||
dispatchCountry: Country
|
||||
|
||||
@Column()
|
||||
dispatchCityId: number
|
||||
|
||||
@ManyToOne(() => City)
|
||||
dispatchCity: City
|
||||
}
|
||||
10
test/functional/decorators/foreign-key/entity/user.ts
Normal file
10
test/functional/decorators/foreign-key/entity/user.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Column, Entity, PrimaryColumn } from "../../../../../src"
|
||||
|
||||
@Entity("users")
|
||||
export class User {
|
||||
@PrimaryColumn({ name: "ref" })
|
||||
id: number
|
||||
|
||||
@Column("uuid", { unique: true })
|
||||
uuid: string
|
||||
}
|
||||
365
test/functional/decorators/foreign-key/foreign-key-decorator.ts
Normal file
365
test/functional/decorators/foreign-key/foreign-key-decorator.ts
Normal file
@ -0,0 +1,365 @@
|
||||
import { DataSource, TableForeignKey, TypeORMError } from "../../../../src"
|
||||
import {
|
||||
closeTestingConnections,
|
||||
createTestingConnections,
|
||||
reloadTestingDatabases,
|
||||
setupSingleTestingConnection,
|
||||
} from "../../../utils/test-utils"
|
||||
import { City } from "./entity/city"
|
||||
import { Country } from "./entity/country"
|
||||
import { Order } from "./entity/order"
|
||||
import { User } from "./entity/user"
|
||||
import { WrongCity } from "./wrong-city"
|
||||
|
||||
describe("decorators > foreign-key", () => {
|
||||
let dataSources: DataSource[]
|
||||
|
||||
before(async () => {
|
||||
dataSources = await createTestingConnections({
|
||||
entities: [__dirname + "/entity/*{.js,.ts}"],
|
||||
})
|
||||
})
|
||||
beforeEach(() => reloadTestingDatabases(dataSources))
|
||||
after(() => closeTestingConnections(dataSources))
|
||||
|
||||
describe("basic functionality", () => {
|
||||
it("should create a foreign keys", () =>
|
||||
Promise.all(
|
||||
dataSources.map(async (dataSource) => {
|
||||
const queryRunner = dataSource.createQueryRunner()
|
||||
const citiesTable = await queryRunner.getTable("cities")
|
||||
const ordersTable = await queryRunner.getTable("orders")
|
||||
await queryRunner.release()
|
||||
|
||||
const narrowForeignKeys = (
|
||||
foreignKeys: TableForeignKey[],
|
||||
) =>
|
||||
foreignKeys.map((foreignKey) => {
|
||||
const {
|
||||
columnNames,
|
||||
referencedColumnNames,
|
||||
referencedTableName,
|
||||
onDelete,
|
||||
onUpdate,
|
||||
} = foreignKey
|
||||
|
||||
return {
|
||||
columnNames: columnNames.sort(),
|
||||
referencedColumnNames:
|
||||
referencedColumnNames.sort(),
|
||||
referencedTableName,
|
||||
onDelete,
|
||||
onUpdate,
|
||||
}
|
||||
})
|
||||
|
||||
const citiesForeignKeys = narrowForeignKeys(
|
||||
citiesTable!.foreignKeys,
|
||||
)
|
||||
|
||||
citiesForeignKeys.length.should.be.equal(1)
|
||||
citiesForeignKeys.should.include.deep.members([
|
||||
{
|
||||
columnNames: ["countryCode"],
|
||||
referencedColumnNames: ["code"],
|
||||
referencedTableName: "countries",
|
||||
onDelete: "CASCADE",
|
||||
onUpdate:
|
||||
dataSource.driver.options.type === "oracle"
|
||||
? "NO ACTION"
|
||||
: "CASCADE",
|
||||
},
|
||||
])
|
||||
|
||||
ordersTable!.foreignKeys.length.should.be.equal(6)
|
||||
const ordersUsersFK = ordersTable!.foreignKeys.find(
|
||||
({ name }) => name === "FK_user_uuid",
|
||||
)
|
||||
|
||||
ordersUsersFK!.should.be.deep.include({
|
||||
columnNames: ["user_uuid"],
|
||||
referencedColumnNames: ["uuid"],
|
||||
referencedTableName: "users",
|
||||
name: "FK_user_uuid",
|
||||
onDelete: "NO ACTION",
|
||||
onUpdate: "NO ACTION",
|
||||
})
|
||||
|
||||
const ordersForeignKeys = narrowForeignKeys(
|
||||
ordersTable!.foreignKeys,
|
||||
)
|
||||
|
||||
ordersForeignKeys.should.include.deep.members([
|
||||
{
|
||||
columnNames: ["countryCode"],
|
||||
referencedColumnNames: ["code"],
|
||||
referencedTableName: "countries",
|
||||
onDelete: "NO ACTION",
|
||||
onUpdate: "NO ACTION",
|
||||
},
|
||||
{
|
||||
columnNames: ["dispatchCountryCode"],
|
||||
referencedColumnNames: ["code"],
|
||||
referencedTableName: "countries",
|
||||
onDelete: "NO ACTION",
|
||||
onUpdate: "NO ACTION",
|
||||
},
|
||||
{
|
||||
columnNames: ["cityId", "countryCode"],
|
||||
referencedColumnNames: ["countryCode", "id"],
|
||||
referencedTableName: "cities",
|
||||
onDelete: "NO ACTION",
|
||||
onUpdate: "NO ACTION",
|
||||
},
|
||||
{
|
||||
columnNames: ["cityId"],
|
||||
referencedColumnNames: ["id"],
|
||||
referencedTableName: "cities",
|
||||
onDelete: "NO ACTION",
|
||||
onUpdate: "NO ACTION",
|
||||
},
|
||||
{
|
||||
columnNames: ["dispatchCityId"],
|
||||
referencedColumnNames: ["id"],
|
||||
referencedTableName: "cities",
|
||||
onDelete: "NO ACTION",
|
||||
onUpdate: "NO ACTION",
|
||||
},
|
||||
])
|
||||
}),
|
||||
))
|
||||
|
||||
it("should persist and load entities", () =>
|
||||
Promise.all(
|
||||
dataSources.map(async (dataSource) => {
|
||||
await dataSource.getRepository(Country).save([
|
||||
{ code: "US", name: "United States" },
|
||||
{ code: "UA", name: "Ukraine" },
|
||||
])
|
||||
|
||||
await dataSource.getRepository(City).save([
|
||||
{ id: 1, name: "New York", countryCode: "US" },
|
||||
{ id: 2, name: "Kiev", countryCode: "UA" },
|
||||
])
|
||||
|
||||
await dataSource.getRepository(User).save([
|
||||
{
|
||||
id: 1,
|
||||
name: "Alice",
|
||||
uuid: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Bob",
|
||||
uuid: "c9bf9e57-1685-4c89-bafb-ff5af830be8a",
|
||||
},
|
||||
])
|
||||
|
||||
await dataSource.getRepository(Order).save([
|
||||
{
|
||||
id: 1,
|
||||
userUuid: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
||||
cityId: 1,
|
||||
countryCode: "US",
|
||||
dispatchCityId: 1,
|
||||
dispatchCountryCode: "US",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
userUuid: "c9bf9e57-1685-4c89-bafb-ff5af830be8a",
|
||||
cityId: 2,
|
||||
countryCode: "UA",
|
||||
dispatchCityId: 1,
|
||||
dispatchCountryCode: "US",
|
||||
},
|
||||
])
|
||||
|
||||
const ordersViaQueryBuilder = await dataSource
|
||||
.createQueryBuilder(Order, "orders")
|
||||
.leftJoinAndSelect(
|
||||
User,
|
||||
"users",
|
||||
"users.uuid = orders.userUuid",
|
||||
)
|
||||
.leftJoinAndSelect(
|
||||
Country,
|
||||
"country",
|
||||
"country.code = orders.countryCode",
|
||||
)
|
||||
.leftJoinAndSelect(
|
||||
"cities",
|
||||
"city",
|
||||
"city.id = orders.cityId",
|
||||
)
|
||||
.leftJoinAndSelect(
|
||||
"orders.dispatchCountry",
|
||||
"dispatchCountry",
|
||||
)
|
||||
.leftJoinAndSelect(
|
||||
"orders.dispatchCity",
|
||||
"dispatchCity",
|
||||
)
|
||||
.orderBy("orders.id", "ASC")
|
||||
.getRawMany()
|
||||
.then((orders) =>
|
||||
orders.map(
|
||||
({
|
||||
orders_id,
|
||||
orders_cityId,
|
||||
orders_dispatchCityId,
|
||||
orders_user_uuid,
|
||||
users_ref,
|
||||
users_uuid,
|
||||
city_id,
|
||||
dispatchCity_id,
|
||||
...order
|
||||
}) => ({
|
||||
orders_id: parseInt(orders_id),
|
||||
orders_cityId: parseInt(orders_cityId),
|
||||
orders_dispatchCityId: parseInt(
|
||||
orders_dispatchCityId,
|
||||
),
|
||||
orders_user_uuid:
|
||||
orders_user_uuid.toLowerCase(),
|
||||
users_ref: parseInt(users_ref),
|
||||
users_uuid: users_uuid.toLowerCase(),
|
||||
city_id: parseInt(city_id),
|
||||
dispatchCity_id: parseInt(dispatchCity_id),
|
||||
...order,
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
ordersViaQueryBuilder.length.should.be.eql(2)
|
||||
|
||||
ordersViaQueryBuilder.should.be.eql([
|
||||
{
|
||||
orders_id: 1,
|
||||
orders_user_uuid:
|
||||
"f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
||||
orders_countryCode: "US",
|
||||
orders_cityId: 1,
|
||||
orders_dispatchCountryCode: "US",
|
||||
orders_dispatchCityId: 1,
|
||||
users_ref: 1,
|
||||
users_uuid: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
||||
country_code: "US",
|
||||
country_name: "United States",
|
||||
city_id: 1,
|
||||
city_countryCode: "US",
|
||||
city_name: "New York",
|
||||
dispatchCountry_code: "US",
|
||||
dispatchCountry_name: "United States",
|
||||
dispatchCity_id: 1,
|
||||
dispatchCity_countryCode: "US",
|
||||
dispatchCity_name: "New York",
|
||||
},
|
||||
{
|
||||
orders_id: 2,
|
||||
orders_user_uuid:
|
||||
"c9bf9e57-1685-4c89-bafb-ff5af830be8a",
|
||||
orders_countryCode: "UA",
|
||||
orders_cityId: 2,
|
||||
orders_dispatchCountryCode: "US",
|
||||
orders_dispatchCityId: 1,
|
||||
users_ref: 2,
|
||||
users_uuid: "c9bf9e57-1685-4c89-bafb-ff5af830be8a",
|
||||
country_code: "UA",
|
||||
country_name: "Ukraine",
|
||||
city_id: 2,
|
||||
city_countryCode: "UA",
|
||||
city_name: "Kiev",
|
||||
dispatchCountry_code: "US",
|
||||
dispatchCountry_name: "United States",
|
||||
dispatchCity_id: 1,
|
||||
dispatchCity_countryCode: "US",
|
||||
dispatchCity_name: "New York",
|
||||
},
|
||||
])
|
||||
|
||||
const ordersViaFind = await dataSource
|
||||
.getRepository(Order)
|
||||
.find({
|
||||
relations: {
|
||||
dispatchCountry: true,
|
||||
dispatchCity: true,
|
||||
},
|
||||
order: { id: "asc" },
|
||||
})
|
||||
.then((orders) =>
|
||||
orders.map(({ userUuid, ...order }) => ({
|
||||
userUuid: userUuid.toLowerCase(),
|
||||
...order,
|
||||
})),
|
||||
)
|
||||
|
||||
ordersViaFind.length.should.be.eql(2)
|
||||
|
||||
ordersViaFind.should.be.eql([
|
||||
{
|
||||
id: 1,
|
||||
userUuid: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
||||
countryCode: "US",
|
||||
cityId: 1,
|
||||
dispatchCountryCode: "US",
|
||||
dispatchCityId: 1,
|
||||
dispatchCountry: {
|
||||
code: "US",
|
||||
name: "United States",
|
||||
},
|
||||
dispatchCity: {
|
||||
id: 1,
|
||||
countryCode: "US",
|
||||
name: "New York",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
userUuid: "c9bf9e57-1685-4c89-bafb-ff5af830be8a",
|
||||
countryCode: "UA",
|
||||
cityId: 2,
|
||||
dispatchCountryCode: "US",
|
||||
dispatchCityId: 1,
|
||||
dispatchCountry: {
|
||||
code: "US",
|
||||
name: "United States",
|
||||
},
|
||||
dispatchCity: {
|
||||
id: 1,
|
||||
countryCode: "US",
|
||||
name: "New York",
|
||||
},
|
||||
},
|
||||
])
|
||||
}),
|
||||
))
|
||||
|
||||
it("should throw an error if referenced entity metadata is not found", async () => {
|
||||
const options = setupSingleTestingConnection("mysql", {
|
||||
entities: [City],
|
||||
})
|
||||
if (!options) return
|
||||
|
||||
await new DataSource(options)
|
||||
.initialize()
|
||||
.should.be.rejectedWith(
|
||||
TypeORMError,
|
||||
"Entity metadata for City#countryCode was not found. Check if you specified a correct entity object and if it's connected in the connection options.",
|
||||
)
|
||||
})
|
||||
|
||||
it("should throw an error if a column in the foreign key is missing", async () => {
|
||||
const options = setupSingleTestingConnection("mysql", {
|
||||
entities: [WrongCity, Country],
|
||||
})
|
||||
if (!options) return
|
||||
|
||||
await new DataSource(options)
|
||||
.initialize()
|
||||
.should.be.rejectedWith(
|
||||
TypeORMError,
|
||||
"Foreign key constraint contains column that is missing in the entity (Country): id",
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
21
test/functional/decorators/foreign-key/wrong-city.ts
Normal file
21
test/functional/decorators/foreign-key/wrong-city.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
ForeignKey,
|
||||
PrimaryColumn,
|
||||
Unique,
|
||||
} from "../../../../src"
|
||||
|
||||
@Entity("wrong_cities")
|
||||
@Unique(["id", "countryCode"])
|
||||
export class WrongCity {
|
||||
@PrimaryColumn()
|
||||
id: number
|
||||
|
||||
@Column({ length: 2 })
|
||||
@ForeignKey("countries", "id", { onDelete: "CASCADE", onUpdate: "CASCADE" })
|
||||
countryCode: string
|
||||
|
||||
@Column()
|
||||
name: string
|
||||
}
|
||||
32
test/functional/entity-schema/foreign-keys/entity/city.ts
Normal file
32
test/functional/entity-schema/foreign-keys/entity/city.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { EntitySchema } from "../../../../../src"
|
||||
import { City } from "../model/city"
|
||||
|
||||
import { CountryEntity } from "./country"
|
||||
|
||||
export const CityEntity = new EntitySchema<City>({
|
||||
name: "City",
|
||||
tableName: "cities",
|
||||
columns: {
|
||||
id: {
|
||||
primary: true,
|
||||
type: Number,
|
||||
},
|
||||
countryCode: {
|
||||
type: String,
|
||||
length: 2,
|
||||
foreignKey: {
|
||||
target: CountryEntity,
|
||||
onDelete: "CASCADE",
|
||||
onUpdate: "CASCADE",
|
||||
},
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
uniques: [
|
||||
{
|
||||
columns: ["id", "countryCode"],
|
||||
},
|
||||
],
|
||||
})
|
||||
17
test/functional/entity-schema/foreign-keys/entity/country.ts
Normal file
17
test/functional/entity-schema/foreign-keys/entity/country.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { EntitySchema } from "../../../../../src"
|
||||
import { Country } from "../model/country"
|
||||
|
||||
export const CountryEntity = new EntitySchema<Country>({
|
||||
name: "Country",
|
||||
tableName: "countries",
|
||||
columns: {
|
||||
code: {
|
||||
primary: true,
|
||||
type: String,
|
||||
length: 2,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
})
|
||||
68
test/functional/entity-schema/foreign-keys/entity/order.ts
Normal file
68
test/functional/entity-schema/foreign-keys/entity/order.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { EntitySchema } from "../../../../../src"
|
||||
import { Order } from "../model/order"
|
||||
import { CityEntity } from "./city"
|
||||
import { CountryEntity } from "./country"
|
||||
|
||||
export const OrderEntity = new EntitySchema<Order>({
|
||||
name: "Order",
|
||||
tableName: "orders",
|
||||
columns: {
|
||||
id: {
|
||||
primary: true,
|
||||
type: Number,
|
||||
},
|
||||
userUuid: {
|
||||
type: "uuid",
|
||||
name: "user_uuid",
|
||||
foreignKey: {
|
||||
target: "User",
|
||||
inverseSide: "uuid",
|
||||
name: "FK_user_uuid",
|
||||
},
|
||||
},
|
||||
countryCode: {
|
||||
type: String,
|
||||
length: 2,
|
||||
foreignKey: {
|
||||
target: CountryEntity,
|
||||
inverseSide: "code",
|
||||
},
|
||||
},
|
||||
cityId: {
|
||||
type: Number,
|
||||
foreignKey: {
|
||||
target: "cities",
|
||||
},
|
||||
},
|
||||
dispatchCountryCode: {
|
||||
type: String,
|
||||
length: 2,
|
||||
},
|
||||
dispatchCityId: {
|
||||
type: Number,
|
||||
},
|
||||
},
|
||||
relations: {
|
||||
dispatchCountry: {
|
||||
type: "many-to-one",
|
||||
target: () => "Country",
|
||||
joinColumn: {
|
||||
name: "dispatchCountryCode",
|
||||
},
|
||||
},
|
||||
dispatchCity: {
|
||||
type: "many-to-one",
|
||||
target: CityEntity,
|
||||
joinColumn: {
|
||||
name: "dispatchCityId",
|
||||
},
|
||||
},
|
||||
},
|
||||
foreignKeys: [
|
||||
{
|
||||
target: CityEntity,
|
||||
columnNames: ["cityId", "countryCode"],
|
||||
referencedColumnNames: ["id", "countryCode"],
|
||||
},
|
||||
],
|
||||
})
|
||||
18
test/functional/entity-schema/foreign-keys/entity/user.ts
Normal file
18
test/functional/entity-schema/foreign-keys/entity/user.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { EntitySchema } from "../../../../../src"
|
||||
import { User } from "../model/user"
|
||||
|
||||
export const UserEntity = new EntitySchema<User>({
|
||||
name: "User",
|
||||
tableName: "users",
|
||||
columns: {
|
||||
id: {
|
||||
primary: true,
|
||||
type: Number,
|
||||
name: "ref",
|
||||
},
|
||||
uuid: {
|
||||
type: "uuid",
|
||||
unique: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
335
test/functional/entity-schema/foreign-keys/foreign-keys-basic.ts
Normal file
335
test/functional/entity-schema/foreign-keys/foreign-keys-basic.ts
Normal file
@ -0,0 +1,335 @@
|
||||
import { DataSource, TableForeignKey } from "../../../../src"
|
||||
import {
|
||||
closeTestingConnections,
|
||||
createTestingConnections,
|
||||
reloadTestingDatabases,
|
||||
} from "../../../utils/test-utils"
|
||||
import { CityEntity } from "./entity/city"
|
||||
import { CountryEntity } from "./entity/country"
|
||||
import { OrderEntity } from "./entity/order"
|
||||
import { UserEntity } from "./entity/user"
|
||||
|
||||
describe("entity-schema > foreign-keys", () => {
|
||||
let dataSources: DataSource[]
|
||||
|
||||
before(async () => {
|
||||
dataSources = await createTestingConnections({
|
||||
entities: [__dirname + "/entity/*{.js,.ts}"],
|
||||
})
|
||||
})
|
||||
beforeEach(() => reloadTestingDatabases(dataSources))
|
||||
after(() => closeTestingConnections(dataSources))
|
||||
|
||||
describe("basic functionality", () => {
|
||||
it("should create a foreign keys", () =>
|
||||
Promise.all(
|
||||
dataSources.map(async (dataSource) => {
|
||||
const queryRunner = dataSource.createQueryRunner()
|
||||
const citiesTable = await queryRunner.getTable("cities")
|
||||
const ordersTable = await queryRunner.getTable("orders")
|
||||
await queryRunner.release()
|
||||
|
||||
const narrowForeignKeys = (
|
||||
foreignKeys: TableForeignKey[],
|
||||
) =>
|
||||
foreignKeys.map((foreignKey) => {
|
||||
const {
|
||||
columnNames,
|
||||
referencedColumnNames,
|
||||
referencedTableName,
|
||||
onDelete,
|
||||
onUpdate,
|
||||
} = foreignKey
|
||||
|
||||
return {
|
||||
columnNames: columnNames.sort(),
|
||||
referencedColumnNames:
|
||||
referencedColumnNames.sort(),
|
||||
referencedTableName,
|
||||
onDelete,
|
||||
onUpdate,
|
||||
}
|
||||
})
|
||||
|
||||
const citiesForeignKeys = narrowForeignKeys(
|
||||
citiesTable!.foreignKeys,
|
||||
)
|
||||
|
||||
citiesForeignKeys.length.should.be.equal(1)
|
||||
citiesForeignKeys.should.include.deep.members([
|
||||
{
|
||||
columnNames: ["countryCode"],
|
||||
referencedColumnNames: ["code"],
|
||||
referencedTableName: "countries",
|
||||
onDelete: "CASCADE",
|
||||
onUpdate:
|
||||
dataSource.driver.options.type === "oracle"
|
||||
? "NO ACTION"
|
||||
: "CASCADE",
|
||||
},
|
||||
])
|
||||
|
||||
ordersTable!.foreignKeys.length.should.be.equal(6)
|
||||
const ordersUsersFK = ordersTable!.foreignKeys.find(
|
||||
({ name }) => name === "FK_user_uuid",
|
||||
)
|
||||
|
||||
ordersUsersFK!.should.be.deep.include({
|
||||
columnNames: ["user_uuid"],
|
||||
referencedColumnNames: ["uuid"],
|
||||
referencedTableName: "users",
|
||||
name: "FK_user_uuid",
|
||||
onDelete: "NO ACTION",
|
||||
onUpdate: "NO ACTION",
|
||||
})
|
||||
|
||||
const ordersForeignKeys = narrowForeignKeys(
|
||||
ordersTable!.foreignKeys,
|
||||
)
|
||||
|
||||
ordersForeignKeys.should.include.deep.members([
|
||||
{
|
||||
columnNames: ["countryCode"],
|
||||
referencedColumnNames: ["code"],
|
||||
referencedTableName: "countries",
|
||||
onDelete: "NO ACTION",
|
||||
onUpdate: "NO ACTION",
|
||||
},
|
||||
{
|
||||
columnNames: ["dispatchCountryCode"],
|
||||
referencedColumnNames: ["code"],
|
||||
referencedTableName: "countries",
|
||||
onDelete: "NO ACTION",
|
||||
onUpdate: "NO ACTION",
|
||||
},
|
||||
{
|
||||
columnNames: ["cityId", "countryCode"],
|
||||
referencedColumnNames: ["countryCode", "id"],
|
||||
referencedTableName: "cities",
|
||||
onDelete: "NO ACTION",
|
||||
onUpdate: "NO ACTION",
|
||||
},
|
||||
{
|
||||
columnNames: ["cityId"],
|
||||
referencedColumnNames: ["id"],
|
||||
referencedTableName: "cities",
|
||||
onDelete: "NO ACTION",
|
||||
onUpdate: "NO ACTION",
|
||||
},
|
||||
{
|
||||
columnNames: ["dispatchCityId"],
|
||||
referencedColumnNames: ["id"],
|
||||
referencedTableName: "cities",
|
||||
onDelete: "NO ACTION",
|
||||
onUpdate: "NO ACTION",
|
||||
},
|
||||
])
|
||||
}),
|
||||
))
|
||||
|
||||
it("should persist and load entities", () =>
|
||||
Promise.all(
|
||||
dataSources.map(async (dataSource) => {
|
||||
await dataSource.getRepository(CountryEntity).save([
|
||||
{ code: "US", name: "United States" },
|
||||
{ code: "UA", name: "Ukraine" },
|
||||
])
|
||||
|
||||
await dataSource.getRepository(CityEntity).save([
|
||||
{ id: 1, name: "New York", countryCode: "US" },
|
||||
{ id: 2, name: "Kiev", countryCode: "UA" },
|
||||
])
|
||||
|
||||
await dataSource.getRepository(UserEntity).save([
|
||||
{
|
||||
id: 1,
|
||||
name: "Alice",
|
||||
uuid: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Bob",
|
||||
uuid: "c9bf9e57-1685-4c89-bafb-ff5af830be8a",
|
||||
},
|
||||
])
|
||||
|
||||
await dataSource.getRepository(OrderEntity).save([
|
||||
{
|
||||
id: 1,
|
||||
userUuid: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
||||
cityId: 1,
|
||||
countryCode: "US",
|
||||
dispatchCityId: 1,
|
||||
dispatchCountryCode: "US",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
userUuid: "c9bf9e57-1685-4c89-bafb-ff5af830be8a",
|
||||
cityId: 2,
|
||||
countryCode: "UA",
|
||||
dispatchCityId: 1,
|
||||
dispatchCountryCode: "US",
|
||||
},
|
||||
])
|
||||
|
||||
const ordersViaQueryBuilder = await dataSource
|
||||
.createQueryBuilder(OrderEntity, "orders")
|
||||
.leftJoinAndSelect(
|
||||
"User",
|
||||
"users",
|
||||
"users.uuid = orders.userUuid",
|
||||
)
|
||||
.leftJoinAndSelect(
|
||||
"Country",
|
||||
"country",
|
||||
"country.code = orders.countryCode",
|
||||
)
|
||||
.leftJoinAndSelect(
|
||||
"cities",
|
||||
"city",
|
||||
"city.id = orders.cityId",
|
||||
)
|
||||
.leftJoinAndSelect(
|
||||
"orders.dispatchCountry",
|
||||
"dispatchCountry",
|
||||
)
|
||||
.leftJoinAndSelect(
|
||||
"orders.dispatchCity",
|
||||
"dispatchCity",
|
||||
)
|
||||
.orderBy("orders.id", "ASC")
|
||||
.getRawMany()
|
||||
.then((orders) =>
|
||||
orders.map(
|
||||
({
|
||||
orders_id,
|
||||
orders_cityId,
|
||||
orders_dispatchCityId,
|
||||
orders_user_uuid,
|
||||
users_ref,
|
||||
users_uuid,
|
||||
city_id,
|
||||
dispatchCity_id,
|
||||
...order
|
||||
}) => ({
|
||||
orders_id: parseInt(orders_id),
|
||||
orders_cityId: parseInt(orders_cityId),
|
||||
orders_dispatchCityId: parseInt(
|
||||
orders_dispatchCityId,
|
||||
),
|
||||
orders_user_uuid:
|
||||
orders_user_uuid.toLowerCase(),
|
||||
users_ref: parseInt(users_ref),
|
||||
users_uuid: users_uuid.toLowerCase(),
|
||||
city_id: parseInt(city_id),
|
||||
dispatchCity_id: parseInt(dispatchCity_id),
|
||||
...order,
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
ordersViaQueryBuilder.length.should.be.eql(2)
|
||||
|
||||
ordersViaQueryBuilder.should.be.eql([
|
||||
{
|
||||
orders_id: 1,
|
||||
orders_user_uuid:
|
||||
"f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
||||
orders_countryCode: "US",
|
||||
orders_cityId: 1,
|
||||
orders_dispatchCountryCode: "US",
|
||||
orders_dispatchCityId: 1,
|
||||
users_ref: 1,
|
||||
users_uuid: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
||||
country_code: "US",
|
||||
country_name: "United States",
|
||||
city_id: 1,
|
||||
city_countryCode: "US",
|
||||
city_name: "New York",
|
||||
dispatchCountry_code: "US",
|
||||
dispatchCountry_name: "United States",
|
||||
dispatchCity_id: 1,
|
||||
dispatchCity_countryCode: "US",
|
||||
dispatchCity_name: "New York",
|
||||
},
|
||||
{
|
||||
orders_id: 2,
|
||||
orders_user_uuid:
|
||||
"c9bf9e57-1685-4c89-bafb-ff5af830be8a",
|
||||
orders_countryCode: "UA",
|
||||
orders_cityId: 2,
|
||||
orders_dispatchCountryCode: "US",
|
||||
orders_dispatchCityId: 1,
|
||||
users_ref: 2,
|
||||
users_uuid: "c9bf9e57-1685-4c89-bafb-ff5af830be8a",
|
||||
country_code: "UA",
|
||||
country_name: "Ukraine",
|
||||
city_id: 2,
|
||||
city_countryCode: "UA",
|
||||
city_name: "Kiev",
|
||||
dispatchCountry_code: "US",
|
||||
dispatchCountry_name: "United States",
|
||||
dispatchCity_id: 1,
|
||||
dispatchCity_countryCode: "US",
|
||||
dispatchCity_name: "New York",
|
||||
},
|
||||
])
|
||||
|
||||
const ordersViaFind = await dataSource
|
||||
.getRepository(OrderEntity)
|
||||
.find({
|
||||
relations: {
|
||||
dispatchCountry: true,
|
||||
dispatchCity: true,
|
||||
},
|
||||
order: { id: "asc" },
|
||||
})
|
||||
.then((orders) =>
|
||||
orders.map(({ userUuid, ...order }) => ({
|
||||
userUuid: userUuid.toLowerCase(),
|
||||
...order,
|
||||
})),
|
||||
)
|
||||
|
||||
ordersViaFind.length.should.be.eql(2)
|
||||
|
||||
ordersViaFind.should.be.eql([
|
||||
{
|
||||
id: 1,
|
||||
userUuid: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
||||
countryCode: "US",
|
||||
cityId: 1,
|
||||
dispatchCountryCode: "US",
|
||||
dispatchCityId: 1,
|
||||
dispatchCountry: {
|
||||
code: "US",
|
||||
name: "United States",
|
||||
},
|
||||
dispatchCity: {
|
||||
id: 1,
|
||||
countryCode: "US",
|
||||
name: "New York",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
userUuid: "c9bf9e57-1685-4c89-bafb-ff5af830be8a",
|
||||
countryCode: "UA",
|
||||
cityId: 2,
|
||||
dispatchCountryCode: "US",
|
||||
dispatchCityId: 1,
|
||||
dispatchCountry: {
|
||||
code: "US",
|
||||
name: "United States",
|
||||
},
|
||||
dispatchCity: {
|
||||
id: 1,
|
||||
countryCode: "US",
|
||||
name: "New York",
|
||||
},
|
||||
},
|
||||
])
|
||||
}),
|
||||
))
|
||||
})
|
||||
})
|
||||
5
test/functional/entity-schema/foreign-keys/model/city.ts
Normal file
5
test/functional/entity-schema/foreign-keys/model/city.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export class City {
|
||||
id: number
|
||||
countryCode: string
|
||||
name: string
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
export class Country {
|
||||
code: string
|
||||
name: string
|
||||
}
|
||||
13
test/functional/entity-schema/foreign-keys/model/order.ts
Normal file
13
test/functional/entity-schema/foreign-keys/model/order.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { City } from "./city"
|
||||
import { Country } from "./country"
|
||||
|
||||
export class Order {
|
||||
id: number
|
||||
userUuid: string
|
||||
countryCode: string
|
||||
cityId: number
|
||||
dispatchCountryCode: string
|
||||
dispatchCountry: Country
|
||||
dispatchCityId: number
|
||||
dispatchCity: City
|
||||
}
|
||||
4
test/functional/entity-schema/foreign-keys/model/user.ts
Normal file
4
test/functional/entity-schema/foreign-keys/model/user.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export class User {
|
||||
id: number
|
||||
uuid: string
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user