mirror of
https://github.com/typeorm/typeorm.git
synced 2025-12-08 21:26:23 +00:00
feat:add utc flag to date column (#11740)
This commit is contained in:
parent
67f793feaa
commit
55cd8e2b08
@ -453,6 +453,7 @@ List of available options in `ColumnOptions`:
|
||||
- `hstoreType: "object"|"string"` - Return type of `HSTORE` column. Returns value as string or as object. Used only in [Postgres](https://www.postgresql.org/docs/9.6/static/hstore.html).
|
||||
- `array: boolean` - Used for postgres and cockroachdb column types which can be array (for example int[])
|
||||
- `transformer: { from(value: DatabaseType): EntityType, to(value: EntityType): DatabaseType }` - Used to marshal properties of arbitrary type `EntityType` into a type `DatabaseType` supported by the database. Array of transformers are also supported and will be applied in natural order when writing, and in reverse order when reading. e.g. `[lowercase, encrypt]` will first lowercase the string then encrypt it when writing, and will decrypt then do nothing when reading.
|
||||
- `utc: boolean` - Indicates if date values should be stored and retrieved in UTC timezone instead of local timezone. Only applies to `date` column type. Default value is `false` (uses local timezone for backward compatibility).
|
||||
|
||||
Note: most of those column options are RDBMS-specific and aren't available in `MongoDB`.
|
||||
|
||||
|
||||
@ -200,4 +200,15 @@ export interface ColumnOptions extends ColumnCommonOptions {
|
||||
* @See https://typeorm.io/decorator-reference#virtualcolumn for more details.
|
||||
*/
|
||||
query?: (alias: string) => string
|
||||
|
||||
/**
|
||||
* Indicates if date values should be stored and retrieved in UTC timezone
|
||||
* instead of local timezone. Only applies to "date" column type.
|
||||
* Default value is "false" (uses local timezone for backward compatibility).
|
||||
*
|
||||
* @example
|
||||
* @Column({ type: "date", utc: true })
|
||||
* birthDate: Date
|
||||
*/
|
||||
utc?: boolean
|
||||
}
|
||||
|
||||
@ -533,7 +533,9 @@ export class AuroraMysqlDriver implements Driver {
|
||||
if (columnMetadata.type === Boolean) {
|
||||
return value === true ? 1 : 0
|
||||
} else if (columnMetadata.type === "date") {
|
||||
return DateUtils.mixedDateToDateString(value)
|
||||
return DateUtils.mixedDateToDateString(value, {
|
||||
utc: columnMetadata.utc,
|
||||
})
|
||||
} else if (columnMetadata.type === "time") {
|
||||
return DateUtils.mixedDateToTimeString(value)
|
||||
} else if (columnMetadata.type === "json") {
|
||||
@ -592,7 +594,9 @@ export class AuroraMysqlDriver implements Driver {
|
||||
) {
|
||||
value = DateUtils.normalizeHydratedDate(value)
|
||||
} else if (columnMetadata.type === "date") {
|
||||
value = DateUtils.mixedDateToDateString(value)
|
||||
value = DateUtils.mixedDateToDateString(value, {
|
||||
utc: columnMetadata.utc,
|
||||
})
|
||||
} else if (columnMetadata.type === "json") {
|
||||
value = typeof value === "string" ? JSON.parse(value) : value
|
||||
} else if (columnMetadata.type === "time") {
|
||||
|
||||
@ -385,7 +385,9 @@ export class CockroachDriver implements Driver {
|
||||
if (columnMetadata.type === Boolean) {
|
||||
return value === true ? 1 : 0
|
||||
} else if (columnMetadata.type === "date") {
|
||||
return DateUtils.mixedDateToDateString(value)
|
||||
return DateUtils.mixedDateToDateString(value, {
|
||||
utc: columnMetadata.utc,
|
||||
})
|
||||
} else if (columnMetadata.type === "time") {
|
||||
return DateUtils.mixedDateToTimeString(value)
|
||||
} else if (
|
||||
@ -445,7 +447,9 @@ export class CockroachDriver implements Driver {
|
||||
) {
|
||||
value = DateUtils.normalizeHydratedDate(value)
|
||||
} else if (columnMetadata.type === "date") {
|
||||
value = DateUtils.mixedDateToDateString(value)
|
||||
value = DateUtils.mixedDateToDateString(value, {
|
||||
utc: columnMetadata.utc,
|
||||
})
|
||||
} else if (columnMetadata.type === "time") {
|
||||
value = DateUtils.mixedTimeToString(value)
|
||||
} else if (columnMetadata.type === "simple-array") {
|
||||
|
||||
@ -616,7 +616,9 @@ export class MysqlDriver implements Driver {
|
||||
if (columnMetadata.type === Boolean) {
|
||||
return value === true ? 1 : 0
|
||||
} else if (columnMetadata.type === "date") {
|
||||
return DateUtils.mixedDateToDateString(value)
|
||||
return DateUtils.mixedDateToDateString(value, {
|
||||
utc: columnMetadata.utc,
|
||||
})
|
||||
} else if (columnMetadata.type === "time") {
|
||||
return DateUtils.mixedDateToTimeString(value)
|
||||
} else if (columnMetadata.type === "json") {
|
||||
@ -670,7 +672,9 @@ export class MysqlDriver implements Driver {
|
||||
) {
|
||||
value = DateUtils.normalizeHydratedDate(value)
|
||||
} else if (columnMetadata.type === "date") {
|
||||
value = DateUtils.mixedDateToDateString(value)
|
||||
value = DateUtils.mixedDateToDateString(value, {
|
||||
utc: columnMetadata.utc,
|
||||
})
|
||||
} else if (columnMetadata.type === "json") {
|
||||
// mysql2 returns JSON values already parsed, but may still be a string
|
||||
// if the JSON value itself is a string (e.g., "\"hello\"")
|
||||
|
||||
@ -531,9 +531,9 @@ export class OracleDriver implements Driver {
|
||||
} else if (columnMetadata.type === "date") {
|
||||
if (typeof value === "string") value = value.replace(/[^0-9-]/g, "")
|
||||
return () =>
|
||||
`TO_DATE('${DateUtils.mixedDateToDateString(
|
||||
value,
|
||||
)}', 'YYYY-MM-DD')`
|
||||
`TO_DATE('${DateUtils.mixedDateToDateString(value, {
|
||||
utc: columnMetadata.utc,
|
||||
})}', 'YYYY-MM-DD')`
|
||||
} else if (
|
||||
columnMetadata.type === Date ||
|
||||
columnMetadata.type === "timestamp" ||
|
||||
@ -567,7 +567,9 @@ export class OracleDriver implements Driver {
|
||||
if (columnMetadata.type === Boolean) {
|
||||
value = !!value
|
||||
} else if (columnMetadata.type === "date") {
|
||||
value = DateUtils.mixedDateToDateString(value)
|
||||
value = DateUtils.mixedDateToDateString(value, {
|
||||
utc: columnMetadata.utc,
|
||||
})
|
||||
} else if (columnMetadata.type === "time") {
|
||||
value = DateUtils.mixedTimeToString(value)
|
||||
} else if (
|
||||
|
||||
@ -656,7 +656,9 @@ export class PostgresDriver implements Driver {
|
||||
if (columnMetadata.type === Boolean) {
|
||||
return value === true ? 1 : 0
|
||||
} else if (columnMetadata.type === "date") {
|
||||
return DateUtils.mixedDateToDateString(value)
|
||||
return DateUtils.mixedDateToDateString(value, {
|
||||
utc: columnMetadata.utc,
|
||||
})
|
||||
} else if (columnMetadata.type === "time") {
|
||||
return DateUtils.mixedDateToTimeString(value)
|
||||
} else if (
|
||||
@ -755,7 +757,9 @@ export class PostgresDriver implements Driver {
|
||||
) {
|
||||
value = DateUtils.normalizeHydratedDate(value)
|
||||
} else if (columnMetadata.type === "date") {
|
||||
value = DateUtils.mixedDateToDateString(value)
|
||||
value = DateUtils.mixedDateToDateString(value, {
|
||||
utc: columnMetadata.utc,
|
||||
})
|
||||
} else if (columnMetadata.type === "time") {
|
||||
value = DateUtils.mixedTimeToString(value)
|
||||
} else if (
|
||||
|
||||
@ -335,7 +335,9 @@ export class ReactNativeDriver implements Driver {
|
||||
) {
|
||||
return value === true ? 1 : 0
|
||||
} else if (columnMetadata.type === "date") {
|
||||
return DateUtils.mixedDateToDateString(value)
|
||||
return DateUtils.mixedDateToDateString(value, {
|
||||
utc: columnMetadata.utc,
|
||||
})
|
||||
} else if (columnMetadata.type === "time") {
|
||||
return DateUtils.mixedDateToTimeString(value)
|
||||
} else if (
|
||||
@ -407,7 +409,9 @@ export class ReactNativeDriver implements Driver {
|
||||
|
||||
value = DateUtils.normalizeHydratedDate(value)
|
||||
} else if (columnMetadata.type === "date") {
|
||||
value = DateUtils.mixedDateToDateString(value)
|
||||
value = DateUtils.mixedDateToDateString(value, {
|
||||
utc: columnMetadata.utc,
|
||||
})
|
||||
} else if (columnMetadata.type === "time") {
|
||||
value = DateUtils.mixedTimeToString(value)
|
||||
} else if (columnMetadata.type === "simple-array") {
|
||||
|
||||
@ -542,7 +542,9 @@ export class SapDriver implements Driver {
|
||||
if (value === null || value === undefined) return value
|
||||
|
||||
if (columnMetadata.type === "date") {
|
||||
return DateUtils.mixedDateToDateString(value)
|
||||
return DateUtils.mixedDateToDateString(value, {
|
||||
utc: columnMetadata.utc,
|
||||
})
|
||||
} else if (columnMetadata.type === "time") {
|
||||
return DateUtils.mixedDateToTimeString(value)
|
||||
} else if (
|
||||
@ -584,7 +586,9 @@ export class SapDriver implements Driver {
|
||||
) {
|
||||
value = DateUtils.normalizeHydratedDate(value)
|
||||
} else if (columnMetadata.type === "date") {
|
||||
value = DateUtils.mixedDateToDateString(value)
|
||||
value = DateUtils.mixedDateToDateString(value, {
|
||||
utc: columnMetadata.utc,
|
||||
})
|
||||
} else if (columnMetadata.type === "time") {
|
||||
value = DateUtils.mixedTimeToString(value)
|
||||
} else if (columnMetadata.type === "simple-array") {
|
||||
|
||||
@ -399,7 +399,9 @@ export class SpannerDriver implements Driver {
|
||||
const lib = this.options.driver || PlatformTools.load("spanner")
|
||||
return lib.Spanner.numeric(value.toString())
|
||||
} else if (columnMetadata.type === "date") {
|
||||
return DateUtils.mixedDateToDateString(value)
|
||||
return DateUtils.mixedDateToDateString(value, {
|
||||
utc: columnMetadata.utc,
|
||||
})
|
||||
} else if (columnMetadata.type === "json") {
|
||||
return value
|
||||
} else if (
|
||||
@ -434,7 +436,9 @@ export class SpannerDriver implements Driver {
|
||||
} else if (columnMetadata.type === "numeric") {
|
||||
value = value.value
|
||||
} else if (columnMetadata.type === "date") {
|
||||
value = DateUtils.mixedDateToDateString(value)
|
||||
value = DateUtils.mixedDateToDateString(value, {
|
||||
utc: columnMetadata.utc,
|
||||
})
|
||||
} else if (columnMetadata.type === "json") {
|
||||
value = typeof value === "string" ? JSON.parse(value) : value
|
||||
} else if (columnMetadata.type === Number) {
|
||||
|
||||
@ -331,7 +331,9 @@ export abstract class AbstractSqliteDriver implements Driver {
|
||||
) {
|
||||
return value === true ? 1 : 0
|
||||
} else if (columnMetadata.type === "date") {
|
||||
return DateUtils.mixedDateToDateString(value)
|
||||
return DateUtils.mixedDateToDateString(value, {
|
||||
utc: columnMetadata.utc,
|
||||
})
|
||||
} else if (columnMetadata.type === "time") {
|
||||
return DateUtils.mixedDateToTimeString(value)
|
||||
} else if (
|
||||
@ -406,7 +408,9 @@ export abstract class AbstractSqliteDriver implements Driver {
|
||||
|
||||
value = DateUtils.normalizeHydratedDate(value)
|
||||
} else if (columnMetadata.type === "date") {
|
||||
value = DateUtils.mixedDateToDateString(value)
|
||||
value = DateUtils.mixedDateToDateString(value, {
|
||||
utc: columnMetadata.utc,
|
||||
})
|
||||
} else if (columnMetadata.type === "time") {
|
||||
value = DateUtils.mixedTimeToString(value)
|
||||
} else if (
|
||||
|
||||
@ -532,7 +532,7 @@ export class SqlServerDriver implements Driver {
|
||||
if (columnMetadata.type === Boolean) {
|
||||
return value === true ? 1 : 0
|
||||
} else if (columnMetadata.type === "date") {
|
||||
return DateUtils.mixedDateToDate(value)
|
||||
return DateUtils.mixedDateToDate(value, columnMetadata.utc)
|
||||
} else if (columnMetadata.type === "time") {
|
||||
return DateUtils.mixedTimeToDate(value)
|
||||
} else if (
|
||||
@ -586,7 +586,9 @@ export class SqlServerDriver implements Driver {
|
||||
) {
|
||||
value = DateUtils.normalizeHydratedDate(value)
|
||||
} else if (columnMetadata.type === "date") {
|
||||
value = DateUtils.mixedDateToDateString(value)
|
||||
value = DateUtils.mixedDateToDateString(value, {
|
||||
utc: columnMetadata.utc,
|
||||
})
|
||||
} else if (columnMetadata.type === "time") {
|
||||
value = DateUtils.mixedTimeToString(value)
|
||||
} else if (columnMetadata.type === "simple-array") {
|
||||
|
||||
@ -123,6 +123,12 @@ export class ColumnMetadata {
|
||||
*/
|
||||
comment?: string
|
||||
|
||||
/**
|
||||
* Indicates if date values use UTC timezone.
|
||||
* Only applies to "date" column type.
|
||||
*/
|
||||
utc: boolean = false
|
||||
|
||||
/**
|
||||
* Default database value.
|
||||
*/
|
||||
@ -388,6 +394,8 @@ export class ColumnMetadata {
|
||||
this.isSelect = options.args.options.select
|
||||
if (options.args.options.insert !== undefined)
|
||||
this.isInsert = options.args.options.insert
|
||||
if (options.args.options.utc !== undefined)
|
||||
this.utc = options.args.options.utc
|
||||
if (options.args.options.update !== undefined)
|
||||
this.isUpdate = options.args.options.update
|
||||
if (options.args.options.readonly !== undefined)
|
||||
|
||||
@ -25,8 +25,21 @@ export class DateUtils {
|
||||
/**
|
||||
* Converts given value into date string in a "YYYY-MM-DD" format.
|
||||
*/
|
||||
static mixedDateToDateString(value: string | Date): string {
|
||||
static mixedDateToDateString(
|
||||
value: string | Date,
|
||||
options?: { utc?: boolean },
|
||||
): string {
|
||||
const utc = options?.utc ?? false
|
||||
if (value instanceof Date) {
|
||||
if (utc) {
|
||||
return (
|
||||
this.formatZerolessValue(value.getUTCFullYear(), 4) +
|
||||
"-" +
|
||||
this.formatZerolessValue(value.getUTCMonth() + 1) +
|
||||
"-" +
|
||||
this.formatZerolessValue(value.getUTCDate())
|
||||
)
|
||||
}
|
||||
return (
|
||||
this.formatZerolessValue(value.getFullYear(), 4) +
|
||||
"-" +
|
||||
|
||||
50
test/functional/columns/date-utc/date-utc.ts
Normal file
50
test/functional/columns/date-utc/date-utc.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import "reflect-metadata"
|
||||
import { expect } from "chai"
|
||||
import {
|
||||
closeTestingConnections,
|
||||
createTestingConnections,
|
||||
reloadTestingDatabases,
|
||||
} from "../../../utils/test-utils"
|
||||
import { Event } from "./entity/Event"
|
||||
import { DataSource } from "../../../../src"
|
||||
|
||||
describe("columns > date utc flag", () => {
|
||||
let originalTZ: string | undefined
|
||||
let connections: DataSource[]
|
||||
|
||||
before(async () => {
|
||||
originalTZ = process.env.TZ
|
||||
process.env.TZ = "America/New_York"
|
||||
connections = await createTestingConnections({
|
||||
entities: [Event],
|
||||
})
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
process.env.TZ = originalTZ
|
||||
await closeTestingConnections(connections)
|
||||
})
|
||||
|
||||
beforeEach(() => reloadTestingDatabases(connections))
|
||||
|
||||
it("should save date columns in UTC when utc flag is true and in local timezone when false", () =>
|
||||
Promise.all(
|
||||
connections.map(async (connection) => {
|
||||
const event = new Event()
|
||||
const testDate = new Date(Date.UTC(2025, 5, 1)) // 2025-06-01 in UTC
|
||||
|
||||
event.localDate = testDate
|
||||
event.utcDate = testDate
|
||||
|
||||
const savedEvent = await connection.manager.save(event)
|
||||
const result = await connection.manager.findOneBy(Event, {
|
||||
id: savedEvent.id,
|
||||
})
|
||||
|
||||
// UTC flag true: should save as 2025-06-01 (UTC date)
|
||||
expect(result!.utcDate).to.equal("2025-06-01")
|
||||
// UTC flag false (default): should save as 2025-05-31 (local timezone)
|
||||
expect(result!.localDate).to.equal("2025-05-31")
|
||||
}),
|
||||
))
|
||||
})
|
||||
17
test/functional/columns/date-utc/entity/Event.ts
Normal file
17
test/functional/columns/date-utc/entity/Event.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { Entity } from "../../../../../src/decorator/entity/Entity"
|
||||
import { PrimaryGeneratedColumn } from "../../../../../src/decorator/columns/PrimaryGeneratedColumn"
|
||||
import { Column } from "../../../../../src/decorator/columns/Column"
|
||||
|
||||
@Entity({
|
||||
name: "event",
|
||||
})
|
||||
export class Event {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number
|
||||
|
||||
@Column({ type: "date" })
|
||||
localDate: Date
|
||||
|
||||
@Column({ type: "date", utc: true })
|
||||
utcDate: Date
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user