Skip to content

Commit

Permalink
feat: new virtual column decorator (#9339)
Browse files Browse the repository at this point in the history
* feat: implement new calculated decorator

This new feature change bahviour of typeorm to allow use new calculated decorator

Closes #9323

* feat: implement new virtual decorator

This new feature change bahviour of typeorm to allow use new virtual decorator

Closes #9323

* feat: Implement new virtual decorator

This new feature change bahviour of typeorm to allow use new virtual decorator

Closes #9323

* feat: implement new virtual decorator

This new feature change bahviour of typeorm to allow use new calculated decorator

Closes #9323

* feat: implement new virtual decorator

This new feature change behavior of typeorm to allow use of the new virtual column decorator

Closes #9323
  • Loading branch information
CollinCashio committed Sep 20, 2022
1 parent 8a837f9 commit d305e5f
Show file tree
Hide file tree
Showing 24 changed files with 658 additions and 39 deletions.
29 changes: 29 additions & 0 deletions docs/decorator-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- [`@DeleteDateColumn`](#deletedatecolumn)
- [`@VersionColumn`](#versioncolumn)
- [`@Generated`](#generated)
- [`@VirtualColumn`](#virtualcolumn)
- [Relation decorators](#relation-decorators)
- [`@OneToOne`](#onetoone)
- [`@ManyToOne`](#manytoone)
Expand Down Expand Up @@ -374,6 +375,34 @@ export class User {

Value will be generated only once, before inserting the entity into the database.

#### `@VirtualColumn`

Special column that is never saved to the database and thus acts as a readonly property.
Each time you call `find` or `findOne` from the entity manager, the value is recalculated based on the query function that was provided in the VirtualColumn Decorator. The alias argument passed to the query references the exact entity alias of the generated query behind the scenes.

```typescript
@Entity({ name: "companies", alias: "COMP" })
export class Company extends BaseEntity {
@PrimaryColumn("varchar", { length: 50 })
name: string;

@VirtualColumn({ query: (alias) => `SELECT COUNT("name") FROM "employees" WHERE "companyName" = ${alias}.name` })
totalEmployeesCount: number;

@OneToMany((type) => Employee, (employee) => employee.company)
employees: Employee[];
}

@Entity({ name: "employees" })
export class Employee extends BaseEntity {
@PrimaryColumn("varchar", { length: 50 })
name: string;

@ManyToOne((type) => Company, (company) => company.employees)
company: Company;
}
```

## Relation decorators

#### `@OneToOne`
Expand Down
74 changes: 74 additions & 0 deletions src/decorator/columns/VirtualColumn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { ColumnType } from "../../driver/types/ColumnTypes"
import { ColumnTypeUndefinedError } from "../../error"
import { getMetadataArgsStorage } from "../../globals"
import { ColumnMetadataArgs } from "../../metadata-args/ColumnMetadataArgs"
import { VirtualColumnOptions } from "../options/VirtualColumnOptions"
/**
* VirtualColumn decorator is used to mark a specific class property as a Virtual column.
*/
export function VirtualColumn(options: VirtualColumnOptions): PropertyDecorator

/**
* VirtualColumn decorator is used to mark a specific class property as a Virtual column.
*/
export function VirtualColumn(
typeOrOptions: ColumnType,
options: VirtualColumnOptions,
): PropertyDecorator

/**
* VirtualColumn decorator is used to mark a specific class property as a Virtual column.
*/
export function VirtualColumn(
typeOrOptions?: ColumnType | VirtualColumnOptions,
options?: VirtualColumnOptions,
): PropertyDecorator {
return function (object: Object, propertyName: string) {
// normalize parameters
let type: ColumnType | undefined
if (typeof typeOrOptions === "string") {
type = <ColumnType>typeOrOptions
} else {
options = <VirtualColumnOptions>typeOrOptions
type = options.type
}

if (!options?.query) {
throw new Error(
"Column options must be defined for calculated columns.",
)
}

// if type is not given explicitly then try to guess it
const reflectMetadataType =
Reflect && (Reflect as any).getMetadata
? (Reflect as any).getMetadata(
"design:type",
object,
propertyName,
)
: undefined
if (!type && reflectMetadataType)
// if type is not given explicitly then try to guess it
type = reflectMetadataType

// check if there is no type in column options then set type from first function argument, or guessed one
if (type) options.type = type

// specify HSTORE type if column is HSTORE
if (options.type === "hstore" && !options.hstoreType)
options.hstoreType =
reflectMetadataType === Object ? "object" : "string"

// if we still don't have a type then we need to give error to user that type is required
if (!options.type)
throw new ColumnTypeUndefinedError(object, propertyName)

getMetadataArgsStorage().columns.push({
target: object.constructor,
propertyName: propertyName,
mode: "virtual-property",
options: options || {},
} as ColumnMetadataArgs)
}
}
31 changes: 31 additions & 0 deletions src/decorator/options/VirtualColumnOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ColumnType } from "../../driver/types/ColumnTypes"
import { ValueTransformer } from "./ValueTransformer"

/**
* Describes all calculated column's options.
*/
export interface VirtualColumnOptions {
/**
* Column type. Must be one of the value from the ColumnTypes class.
*/
type?: ColumnType

/**
* Return type of HSTORE column.
* Returns value as string or as object.
*/
hstoreType?: "object" | "string"

/**
* Query to be used to populate the column data. This query is used when generating the relational db script.
* The query function is called with the current entities alias either defined by the Entity Decorator or automatically
* @See https://typeorm.io/decorator-reference#virtualcolumn for more details.
*/
query: (alias: string) => string

/**
* Specifies a value transformer(s) that is to be used to unmarshal
* this column when reading from the database.
*/
transformer?: ValueTransformer | ValueTransformer[]
}
3 changes: 3 additions & 0 deletions src/driver/aurora-mysql/AuroraMysqlDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,9 @@ export class AuroraMysqlDriver implements Driver {
) {
// convert to number if that exists in possible enum options
value = parseInt(value)
} else if (columnMetadata.type === Number) {
// convert to number if number
value = !isNaN(+value) ? parseInt(value) : value
}

if (columnMetadata.transformer)
Expand Down
6 changes: 6 additions & 0 deletions src/driver/mysql/MysqlDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,9 @@ export class MysqlDriver implements Driver {
return "" + value
} else if (columnMetadata.type === "set") {
return DateUtils.simpleArrayToString(value)
} else if (columnMetadata.type === Number) {
// convert to number if number
value = !isNaN(+value) ? parseInt(value) : value
}

return value
Expand Down Expand Up @@ -683,6 +686,9 @@ export class MysqlDriver implements Driver {
value = parseInt(value)
} else if (columnMetadata.type === "set") {
value = DateUtils.stringToSimpleArray(value)
} else if (columnMetadata.type === Number) {
// convert to number if number
value = !isNaN(+value) ? parseInt(value) : value
}

if (columnMetadata.transformer)
Expand Down
3 changes: 3 additions & 0 deletions src/driver/oracle/OracleDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,9 @@ export class OracleDriver implements Driver {
value = DateUtils.stringToSimpleArray(value)
} else if (columnMetadata.type === "simple-json") {
value = DateUtils.stringToSimpleJson(value)
} else if (columnMetadata.type === Number) {
// convert to number if number
value = !isNaN(+value) ? parseInt(value) : value
}

if (columnMetadata.transformer)
Expand Down
3 changes: 3 additions & 0 deletions src/driver/postgres/PostgresDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,9 @@ export class PostgresDriver implements Driver {
? parseInt(value)
: value
}
} else if (columnMetadata.type === Number) {
// convert to number if number
value = !isNaN(+value) ? parseInt(value) : value
}

if (columnMetadata.transformer)
Expand Down
3 changes: 3 additions & 0 deletions src/driver/sap/SapDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,9 @@ export class SapDriver implements Driver {
value = DateUtils.stringToSimpleJson(value)
} else if (columnMetadata.type === "simple-enum") {
value = DateUtils.stringToSimpleEnum(value, columnMetadata)
} else if (columnMetadata.type === Number) {
// convert to number if number
value = !isNaN(+value) ? parseInt(value) : value
}

if (columnMetadata.transformer)
Expand Down
3 changes: 3 additions & 0 deletions src/driver/spanner/SpannerDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,9 @@ export class SpannerDriver implements Driver {
value = DateUtils.mixedDateToDateString(value)
} else if (columnMetadata.type === "json") {
value = typeof value === "string" ? JSON.parse(value) : value
} else if (columnMetadata.type === Number) {
// convert to number if number
value = !isNaN(+value) ? parseInt(value) : value
}

if (columnMetadata.transformer)
Expand Down
3 changes: 3 additions & 0 deletions src/driver/sqlite-abstract/AbstractSqliteDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,9 @@ export abstract class AbstractSqliteDriver implements Driver {
value = DateUtils.stringToSimpleJson(value)
} else if (columnMetadata.type === "simple-enum") {
value = DateUtils.stringToSimpleEnum(value, columnMetadata)
} else if (columnMetadata.type === Number) {
// convert to number if number
value = !isNaN(+value) ? parseInt(value) : value
}

if (columnMetadata.transformer)
Expand Down
3 changes: 3 additions & 0 deletions src/driver/sqlserver/SqlServerDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,9 @@ export class SqlServerDriver implements Driver {
value = DateUtils.stringToSimpleJson(value)
} else if (columnMetadata.type === "simple-enum") {
value = DateUtils.stringToSimpleEnum(value, columnMetadata)
} else if (columnMetadata.type === Number) {
// convert to number if number
value = !isNaN(+value) ? parseInt(value) : value
}

if (columnMetadata.transformer)
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export * from "./decorator/columns/PrimaryGeneratedColumn"
export * from "./decorator/columns/PrimaryColumn"
export * from "./decorator/columns/UpdateDateColumn"
export * from "./decorator/columns/VersionColumn"
export * from "./decorator/columns/VirtualColumn"
export * from "./decorator/columns/ViewColumn"
export * from "./decorator/columns/ObjectIdColumn"
export * from "./decorator/listeners/AfterInsert"
Expand Down
1 change: 1 addition & 0 deletions src/metadata-args/types/ColumnMode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
export type ColumnMode =
| "regular"
| "virtual"
| "virtual-property"
| "createDate"
| "updateDate"
| "deleteDate"
Expand Down
48 changes: 29 additions & 19 deletions src/metadata-builder/EntityMetadataValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,29 +123,39 @@ export class EntityMetadataValidator {
})

if (!(driver.options.type === "mongodb")) {
entityMetadata.columns.forEach((column) => {
const normalizedColumn = driver.normalizeType(
column,
) as ColumnType
if (driver.supportedDataTypes.indexOf(normalizedColumn) === -1)
throw new DataTypeNotSupportedError(
entityMetadata.columns
.filter((column) => !column.isVirtualProperty)
.forEach((column) => {
const normalizedColumn = driver.normalizeType(
column,
normalizedColumn,
driver.options.type,
)
if (
column.length &&
driver.withLengthColumnTypes.indexOf(normalizedColumn) ===
) as ColumnType
if (
driver.supportedDataTypes.indexOf(normalizedColumn) ===
-1
)
throw new TypeORMError(
`Column ${column.propertyName} of Entity ${entityMetadata.name} does not support length property.`,
)
if (column.type === "enum" && !column.enum && !column.enumName)
throw new TypeORMError(
`Column "${column.propertyName}" of Entity "${entityMetadata.name}" is defined as enum, but missing "enum" or "enumName" properties.`,
throw new DataTypeNotSupportedError(
column,
normalizedColumn,
driver.options.type,
)
if (
column.length &&
driver.withLengthColumnTypes.indexOf(
normalizedColumn,
) === -1
)
})
throw new TypeORMError(
`Column ${column.propertyName} of Entity ${entityMetadata.name} does not support length property.`,
)
if (
column.type === "enum" &&
!column.enum &&
!column.enumName
)
throw new TypeORMError(
`Column "${column.propertyName}" of Entity "${entityMetadata.name}" is defined as enum, but missing "enum" or "enumName" properties.`,
)
})
}

if (
Expand Down
22 changes: 22 additions & 0 deletions src/metadata/ColumnMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ValueTransformer } from "../decorator/options/ValueTransformer"
import { ApplyValueTransformers } from "../util/ApplyValueTransformers"
import { ObjectUtils } from "../util/ObjectUtils"
import { InstanceChecker } from "../util/InstanceChecker"
import { VirtualColumnOptions } from "../decorator/options/VirtualColumnOptions"

/**
* This metadata contains all information about entity's column.
Expand Down Expand Up @@ -238,6 +239,20 @@ export class ColumnMetadata {
*/
isVirtual: boolean = false

/**
* Indicates if column is a virtual property. Virtual properties are not mapped to the entity.
* This property is used in tandem the virtual column decorator.
* @See https://typeorm.io/decorator-reference#virtualcolumn for more details.
*/
isVirtualProperty: boolean = false

/**
* Query to be used to populate the column data. This query is used when generating the relational db script.
* The query function is called with the current entities alias either defined by the Entity Decorator or automatically
* @See https://typeorm.io/decorator-reference#virtualcolumn for more details.
*/
query?: (alias: string) => string

/**
* Indicates if column is discriminator. Discriminator columns are not mapped to the entity.
*/
Expand Down Expand Up @@ -448,6 +463,7 @@ export class ColumnMetadata {
if (options.args.options.array)
this.isArray = options.args.options.array
if (options.args.mode) {
this.isVirtualProperty = options.args.mode === "virtual-property"
this.isVirtual = options.args.mode === "virtual"
this.isTreeLevel = options.args.mode === "treeLevel"
this.isCreateDate = options.args.mode === "createDate"
Expand All @@ -456,12 +472,18 @@ export class ColumnMetadata {
this.isVersion = options.args.mode === "version"
this.isObjectId = options.args.mode === "objectId"
}
if (this.isVirtualProperty) {
this.isInsert = false
this.isUpdate = false
}
if (options.args.options.transformer)
this.transformer = options.args.options.transformer
if (options.args.options.spatialFeatureType)
this.spatialFeatureType = options.args.options.spatialFeatureType
if (options.args.options.srid !== undefined)
this.srid = options.args.options.srid
if ((options.args.options as VirtualColumnOptions).query)
this.query = (options.args.options as VirtualColumnOptions).query
if (this.isTreeLevel)
this.type = options.connection.driver.mappedDataTypes.treeLevel
if (this.isCreateDate) {
Expand Down

0 comments on commit d305e5f

Please sign in to comment.