feat: add support for STI on EntitySchema (#9834)

* feat: add support for STI on EntitySchema

Closes: #9833

* fix: run prettier

---------

Co-authored-by: Umed Khudoiberdiev <pleerock.me@gmail.com>
This commit is contained in:
Gabriel Kim 2023-04-06 04:23:45 -03:00 committed by GitHub
parent 97280fc825
commit bc306fb5a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 324 additions and 6 deletions

View File

@ -230,6 +230,112 @@ export const UserEntitySchema = new EntitySchema<User>({
Be sure to add the `extended` columns also to the `Category` interface (e.g., via `export interface Category extend BaseEntity`).
### Single Table Inheritance
In order to use [Single Table Inheritance](entity-inheritance.md#single-table-inheritance):
1. Add the `inheritance` option to the **parent** class schema, specifying the inheritance pattern ("STI") and the
**discriminator** column, which will store the name of the *child* class on each row
2. Set the `type: "entity-child"` option for all **children** classes' schemas, while extending the *parent* class
columns using the spread operator syntax described above
```ts
// entity.ts
export abstract class Base {
id!: number
type!: string
createdAt!: Date
updatedAt!: Date
}
export class A extends Base {
constructor(public a: boolean) {
super()
}
}
export class B extends Base {
constructor(public b: number) {
super()
}
}
export class C extends Base {
constructor(public c: string) {
super()
}
}
```
```ts
// schema.ts
const BaseSchema = new EntitySchema<Base>({
target: Base,
name: "Base",
columns: {
id: {
type: Number,
primary: true,
generated: "increment",
},
type: {
type: String,
},
createdAt: {
type: Date,
createDate: true,
},
updatedAt: {
type: Date,
updateDate: true,
},
},
// NEW: Inheritance options
inheritance: {
pattern: "STI",
column: "type",
},
})
const ASchema = new EntitySchema<A>({
target: A,
name: "A",
type: "entity-child",
columns: {
...BaseSchema.options.columns,
a: {
type: Boolean,
},
},
})
const BSchema = new EntitySchema<B>({
target: B,
name: "B",
type: "entity-child",
columns: {
...BaseSchema.options.columns,
b: {
type: Number,
},
},
})
const CSchema = new EntitySchema<C>({
target: C,
name: "C",
type: "entity-child",
columns: {
...BaseSchema.options.columns,
c: {
type: String,
},
},
})
```
## Using Schemas to Query / Insert Data
Of course, you can use the defined schemas in your repositories or entity manager as you would use the decorators.

2
package-lock.json generated
View File

@ -1,7 +1,7 @@
{
"name": "typeorm",
"version": "0.3.12",
"lockfileVersion": 2,
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {

View File

@ -0,0 +1,13 @@
import { ColumnOptions } from "../decorator/options/ColumnOptions"
export interface EntitySchemaInheritanceOptions {
/**
* Inheritance pattern.
*/
pattern?: "STI"
/**
* Inheritance discriminator column.
*/
column?: string | ColumnOptions
}

View File

@ -11,17 +11,13 @@ import { TableType } from "../metadata/types/TableTypes"
import { EntitySchemaUniqueOptions } from "./EntitySchemaUniqueOptions"
import { EntitySchemaCheckOptions } from "./EntitySchemaCheckOptions"
import { EntitySchemaExclusionOptions } from "./EntitySchemaExclusionOptions"
import { EntitySchemaInheritanceOptions } from "./EntitySchemaInheritanceOptions"
import { EntitySchemaRelationIdOptions } from "./EntitySchemaRelationIdOptions"
/**
* Interface for entity metadata mappings stored inside "schemas" instead of models decorated by decorators.
*/
export class EntitySchemaOptions<T> {
/**
* Name of the schema it extends.
*/
extends?: string
/**
* Target bind to this entity schema. Optional.
*/
@ -123,4 +119,9 @@ export class EntitySchemaOptions<T> {
* View expression.
*/
expression?: string | ((connection: DataSource) => SelectQueryBuilder<any>)
/**
* Inheritance options.
*/
inheritance?: EntitySchemaInheritanceOptions
}

View File

@ -16,6 +16,7 @@ import { ExclusionMetadataArgs } from "../metadata-args/ExclusionMetadataArgs"
import { EntitySchemaColumnOptions } from "./EntitySchemaColumnOptions"
import { EntitySchemaOptions } from "./EntitySchemaOptions"
import { EntitySchemaEmbeddedError } from "./EntitySchemaEmbeddedError"
import { InheritanceMetadataArgs } from "../metadata-args/InheritanceMetadataArgs"
import { RelationIdMetadataArgs } from "../metadata-args/RelationIdMetadataArgs"
/**
@ -50,6 +51,20 @@ export class EntitySchemaTransformer {
}
metadataArgsStorage.tables.push(tableMetadata)
const { inheritance } = options
if (inheritance) {
metadataArgsStorage.inheritances.push({
target: options.target,
pattern: inheritance.pattern ?? "STI",
column: inheritance.column
? typeof inheritance.column === "string"
? { name: inheritance.column }
: inheritance.column
: undefined,
} as InheritanceMetadataArgs)
}
this.transformColumnsRecursive(options, metadataArgsStorage)
})

View File

@ -0,0 +1,7 @@
import { Base } from "./Base"
export class A extends Base {
constructor(public a: boolean) {
super()
}
}

View File

@ -0,0 +1,7 @@
import { Base } from "./Base"
export class B extends Base {
constructor(public b: number) {
super()
}
}

View File

@ -0,0 +1,6 @@
export abstract class Base {
id!: number
type!: string
createdAt!: Date
updatedAt!: Date
}

View File

@ -0,0 +1,7 @@
import { Base } from "./Base"
export class C extends Base {
constructor(public c: string) {
super()
}
}

View File

@ -0,0 +1,5 @@
export * from "./Base"
export * from "./A"
export * from "./B"
export * from "./C"

View File

@ -0,0 +1,65 @@
import "reflect-metadata"
import { expect } from "chai"
import {
createTestingConnections,
closeTestingConnections,
reloadTestingDatabases,
} from "../../utils/test-utils"
import { DataSource } from "../../../src/data-source/DataSource"
import { Repository } from "../../../src"
import { Base, A, B, C } from "./entity"
import { BaseSchema, ASchema, BSchema, CSchema } from "./schema"
describe("github issues > #9833 Add support for Single Table Inheritance when using Entity Schemas", () => {
let dataSources: DataSource[]
before(
async () =>
(dataSources = await createTestingConnections({
entities: [BaseSchema, ASchema, BSchema, CSchema],
schemaCreate: true,
dropSchema: true,
enabledDrivers: [
"better-sqlite3",
"cockroachdb",
"mariadb",
"mssql",
"mysql",
"oracle",
"postgres",
"spanner",
"sqlite",
],
})),
)
beforeEach(() => reloadTestingDatabases(dataSources))
after(() => closeTestingConnections(dataSources))
it("should instantiate concrete entities when using EntitySchema", () =>
Promise.all(
dataSources.map(async (dataSource) => {
// Arrange
const repository: Repository<Base> =
dataSource.getRepository(Base)
const entities: Base[] = [new A(true), new B(42), new C("foo")]
await repository.save(entities)
// Act
const loadedEntities = await repository.find({
order: { type: "ASC" },
})
// Assert
expect(loadedEntities[0]).to.be.instanceOf(A)
expect(loadedEntities[1]).to.be.instanceOf(B)
expect(loadedEntities[2]).to.be.instanceOf(C)
}),
))
})

View File

@ -0,0 +1,17 @@
import { EntitySchema } from "../../../../src"
import { A } from "../entity"
import { BaseSchema } from "./Base"
export const ASchema = new EntitySchema<A>({
target: A,
name: "A",
type: "entity-child",
columns: {
...BaseSchema.options.columns,
a: {
type: Boolean,
},
},
})

View File

@ -0,0 +1,17 @@
import { EntitySchema } from "../../../../src"
import { B } from "../entity"
import { BaseSchema } from "./Base"
export const BSchema = new EntitySchema<B>({
target: B,
name: "B",
type: "entity-child",
columns: {
...BaseSchema.options.columns,
b: {
type: Number,
},
},
})

View File

@ -0,0 +1,30 @@
import { EntitySchema } from "../../../../src"
import { Base } from "../entity"
export const BaseSchema = new EntitySchema<Base>({
target: Base,
name: "Base",
columns: {
id: {
type: Number,
primary: true,
generated: "increment",
},
type: {
type: String,
},
createdAt: {
type: Date,
createDate: true,
},
updatedAt: {
type: Date,
updateDate: true,
},
},
inheritance: {
pattern: "STI",
column: "type",
},
})

View File

@ -0,0 +1,17 @@
import { EntitySchema } from "../../../../src"
import { C } from "../entity"
import { BaseSchema } from "./Base"
export const CSchema = new EntitySchema<C>({
target: C,
name: "C",
type: "entity-child",
columns: {
...BaseSchema.options.columns,
c: {
type: String,
},
},
})

View File

@ -0,0 +1,5 @@
export * from "./Base"
export * from "./A"
export * from "./B"
export * from "./C"