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:
Yevhen Komarov 2025-04-04 00:22:40 +03:00 committed by GitHub
parent 055eafd2e4
commit 6ebae3b795
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 1910 additions and 1 deletions

View File

@ -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
}
```

View File

@ -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"],
},
],
})
```

View File

@ -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;

View 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),
)

View 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
}

View File

@ -0,0 +1,10 @@
import { Column, Entity, PrimaryColumn } from "../../../src"
@Entity("countries")
export class Country {
@PrimaryColumn({ length: 2 })
code: string
@Column()
name: string
}

View 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
}

View 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
}

View 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),
)

View 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"],
},
],
})

View 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,
},
},
})

View 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"],
},
],
})

View 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,
},
},
})

View File

@ -0,0 +1,5 @@
export class City {
id: number
countryCode: string
name: string
}

View File

@ -0,0 +1,4 @@
export class Country {
code: string
name: string
}

View 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
}

View File

@ -0,0 +1,4 @@
export class User {
id: number
uuid: string
}

104
src/decorator/ForeignKey.ts Normal file
View 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)
}
}

View 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
}

View 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
}

View File

@ -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
}

View 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[]
}

View File

@ -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.
*/

View File

@ -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) => {

View File

@ -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"

View 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
}

View File

@ -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(

View File

@ -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,
}),
)
})
}
}

View 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
}

View File

@ -0,0 +1,10 @@
import { Column, Entity, PrimaryColumn } from "../../../../../src"
@Entity("countries")
export class Country {
@PrimaryColumn({ length: 2 })
code: string
@Column()
name: string
}

View 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
}

View 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
}

View 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",
)
})
})
})

View 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
}

View 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"],
},
],
})

View 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,
},
},
})

View 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"],
},
],
})

View 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,
},
},
})

View 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",
},
},
])
}),
))
})
})

View File

@ -0,0 +1,5 @@
export class City {
id: number
countryCode: string
name: string
}

View File

@ -0,0 +1,4 @@
export class Country {
code: string
name: string
}

View 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
}

View File

@ -0,0 +1,4 @@
export class User {
id: number
uuid: string
}