velocious 1.0.456 → 1.0.457

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +1 -1
  2. package/build/cli/commands/db/drop.js +7 -3
  3. package/build/database/datetime-storage.js +188 -0
  4. package/build/database/drivers/base.js +2 -2
  5. package/build/database/drivers/mssql/index.js +4 -0
  6. package/build/database/drivers/mysql/index.js +11 -1
  7. package/build/database/drivers/pgsql/index.js +15 -3
  8. package/build/database/migration/index.js +127 -0
  9. package/build/database/record/index.js +6 -63
  10. package/build/frontend-model-controller.js +4 -2
  11. package/build/frontend-models/base.js +7 -0
  12. package/build/src/cli/commands/db/drop.d.ts.map +1 -1
  13. package/build/src/cli/commands/db/drop.js +10 -4
  14. package/build/src/database/datetime-storage.d.ts +56 -0
  15. package/build/src/database/datetime-storage.d.ts.map +1 -0
  16. package/build/src/database/datetime-storage.js +174 -0
  17. package/build/src/database/drivers/base.d.ts.map +1 -1
  18. package/build/src/database/drivers/base.js +3 -3
  19. package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
  20. package/build/src/database/drivers/mssql/index.js +4 -1
  21. package/build/src/database/drivers/mysql/index.d.ts +5 -0
  22. package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
  23. package/build/src/database/drivers/mysql/index.js +10 -2
  24. package/build/src/database/drivers/pgsql/index.d.ts +5 -0
  25. package/build/src/database/drivers/pgsql/index.d.ts.map +1 -1
  26. package/build/src/database/drivers/pgsql/index.js +13 -3
  27. package/build/src/database/migration/index.d.ts +66 -0
  28. package/build/src/database/migration/index.d.ts.map +1 -1
  29. package/build/src/database/migration/index.js +112 -1
  30. package/build/src/database/record/index.d.ts.map +1 -1
  31. package/build/src/database/record/index.js +7 -52
  32. package/build/src/frontend-model-controller.d.ts.map +1 -1
  33. package/build/src/frontend-model-controller.js +5 -3
  34. package/build/src/frontend-models/base.d.ts.map +1 -1
  35. package/build/src/frontend-models/base.js +6 -1
  36. package/build/tsconfig.tsbuildinfo +1 -1
  37. package/package.json +1 -1
  38. package/src/cli/commands/db/drop.js +7 -3
  39. package/src/database/datetime-storage.js +188 -0
  40. package/src/database/drivers/base.js +2 -2
  41. package/src/database/drivers/mssql/index.js +4 -0
  42. package/src/database/drivers/mysql/index.js +11 -1
  43. package/src/database/drivers/pgsql/index.js +15 -3
  44. package/src/database/migration/index.js +127 -0
  45. package/src/database/record/index.js +6 -63
  46. package/src/frontend-model-controller.js +4 -2
  47. package/src/frontend-models/base.js +7 -0
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  * Database models with migrations and validations
6
6
  * Database models that work almost the same in frontend and backend
7
7
  * Declarative state machines for models (see [docs/state-machine.md](docs/state-machine.md))
8
- * Migrations for schema changes (see [docs/database-migrations.md](docs/database-migrations.md))
8
+ * Migrations for schema changes and UTC datetime storage (see [docs/database-migrations.md](docs/database-migrations.md))
9
9
  * Controllers and views for HTTP endpoints
10
10
  * Frontend-model transport for creating, updating, querying, and subscribing to query-filtered lifecycle events over HTTP/WebSocket, with structured per-attribute validation error responses (see [docs/frontend-models.md](docs/frontend-models.md))
11
11
  * Expo / Metro compatibility guidance and a real Expo export check (see [docs/expo-metro-compatibility.md](docs/expo-metro-compatibility.md))
@@ -32,9 +32,13 @@ export default class DbDrop extends DbBaseCommand {
32
32
  const configuredFallback = newConfiguration.useDatabase
33
33
  const useConfiguredFallback = typeof configuredFallback == "string" && configuredFallback.length > 0 && configuredFallback != targetDatabaseName
34
34
 
35
- newConfiguration.database = useConfiguredFallback
36
- ? configuredFallback
37
- : this.systemFallbackDatabaseName(databaseType)
35
+ if (useConfiguredFallback) {
36
+ newConfiguration.database = configuredFallback
37
+ } else if (databaseType == "mysql") {
38
+ delete newConfiguration.database
39
+ } else {
40
+ newConfiguration.database = this.systemFallbackDatabaseName(databaseType)
41
+ }
38
42
 
39
43
  if (databaseType == "mssql" && newConfiguration.sqlConfig?.database) {
40
44
  delete newConfiguration.sqlConfig.database
@@ -0,0 +1,188 @@
1
+ // @ts-check
2
+
3
+ import isDate from "../utils/is-date.js"
4
+
5
+ const dateTimeWithTimezonePattern = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:[zZ]|[+-]\d{2}:\d{2})$/
6
+ const dateTimeWithoutTimezonePattern = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?$/
7
+ const timezoneSuffixPattern = /(?:[zZ]|[+-]\d{2}:\d{2})$/
8
+
9
+ /**
10
+ * Pads a numeric date/time part.
11
+ * @param {number} value - Numeric part.
12
+ * @param {number} length - Target length.
13
+ * @returns {string} - Padded part.
14
+ */
15
+ function pad(value, length = 2) {
16
+ return String(value).padStart(length, "0")
17
+ }
18
+
19
+ /**
20
+ * Replaces SQL-style datetime separators with ISO separators for parsing.
21
+ * @param {string} value - Datetime string.
22
+ * @returns {string} - Datetime string with a `T` separator.
23
+ */
24
+ function normalizeDateTimeSeparator(value) {
25
+ return value.includes("T") ? value : value.replace(" ", "T")
26
+ }
27
+
28
+ /**
29
+ * Parses a datetime string with an explicit timezone.
30
+ * @param {string} value - Datetime string.
31
+ * @returns {Date | string} - Parsed date or the original string when it is not a recognized datetime.
32
+ */
33
+ function parseTimezoneQualifiedDateTimeString(value) {
34
+ if (!dateTimeWithTimezonePattern.test(value)) return value
35
+
36
+ const timestamp = Date.parse(normalizeDateTimeSeparator(value))
37
+
38
+ if (Number.isNaN(timestamp)) return value
39
+
40
+ return new Date(timestamp)
41
+ }
42
+
43
+ /**
44
+ * Parses a timezone-less datetime string as UTC.
45
+ * @param {string} value - Datetime string.
46
+ * @returns {Date | string} - Parsed date or the original string when it is not a recognized datetime.
47
+ */
48
+ function parseTimezoneLessDateTimeStringAsUtc(value) {
49
+ if (!dateTimeWithoutTimezonePattern.test(value)) return value
50
+
51
+ const timestamp = Date.parse(`${normalizeDateTimeSeparator(value)}Z`)
52
+
53
+ if (Number.isNaN(timestamp)) return value
54
+
55
+ return new Date(timestamp)
56
+ }
57
+
58
+ /**
59
+ * Parses a timezone-less datetime string as the current runtime's local wall-clock time.
60
+ * @param {string} value - Datetime string.
61
+ * @returns {Date | string} - Parsed date or the original string when it is not a recognized datetime.
62
+ */
63
+ function parseTimezoneLessDateTimeStringAsLocal(value) {
64
+ if (!dateTimeWithoutTimezonePattern.test(value)) return value
65
+
66
+ const timestamp = Date.parse(normalizeDateTimeSeparator(value))
67
+
68
+ if (Number.isNaN(timestamp)) return value
69
+
70
+ return new Date(timestamp)
71
+ }
72
+
73
+ /**
74
+ * Parses a timezone-less legacy datetime string with an explicit local offset.
75
+ * The offset follows JavaScript's `Date#getTimezoneOffset()` sign convention.
76
+ * @param {string} value - Datetime string.
77
+ * @param {number} legacyLocalOffsetMinutes - UTC-minus-local offset in minutes.
78
+ * @returns {Date | string} - Parsed date or the original string when it is not a recognized datetime.
79
+ */
80
+ function parseTimezoneLessDateTimeStringWithOffset(value, legacyLocalOffsetMinutes) {
81
+ const utcDate = parseTimezoneLessDateTimeStringAsUtc(value)
82
+
83
+ if (!isDate(utcDate)) return value
84
+
85
+ return new Date(utcDate.getTime() + (legacyLocalOffsetMinutes * 60 * 1000))
86
+ }
87
+
88
+ /**
89
+ * Checks whether a string has a datetime timezone suffix.
90
+ * @param {string} value - Value to check.
91
+ * @returns {boolean} - Whether the string ends with `Z` or an offset.
92
+ */
93
+ export function hasDateTimeTimezone(value) {
94
+ return timezoneSuffixPattern.test(value)
95
+ }
96
+
97
+ /**
98
+ * Formats a Date for database storage as a UTC instant.
99
+ * @param {Date} value - Date value.
100
+ * @param {object} args - Options.
101
+ * @param {string} args.databaseType - Database driver type.
102
+ * @returns {string} - Database datetime string.
103
+ */
104
+ export function formatDateForDatabase(value, {databaseType}) {
105
+ if (databaseType == "sqlite") return value.toISOString()
106
+
107
+ return [
108
+ value.getUTCFullYear(),
109
+ "-",
110
+ pad(value.getUTCMonth() + 1),
111
+ "-",
112
+ pad(value.getUTCDate()),
113
+ " ",
114
+ pad(value.getUTCHours()),
115
+ ":",
116
+ pad(value.getUTCMinutes()),
117
+ ":",
118
+ pad(value.getUTCSeconds()),
119
+ ".",
120
+ pad(value.getUTCMilliseconds(), 3)
121
+ ].join("")
122
+ }
123
+
124
+ /**
125
+ * Normalizes a record write string into a Date when it is a recognized datetime string.
126
+ * Timezone-less strings are external input and are treated as UTC.
127
+ * @param {string} value - Value to normalize.
128
+ * @returns {Date | string} - Normalized value.
129
+ */
130
+ export function normalizeDateStringForWrite(value) {
131
+ if (hasDateTimeTimezone(value)) return parseTimezoneQualifiedDateTimeString(value)
132
+
133
+ return parseTimezoneLessDateTimeStringAsUtc(value)
134
+ }
135
+
136
+ /**
137
+ * Normalizes a record write value into a Date when it is a recognized datetime string.
138
+ * Timezone-less strings are external input and are treated as UTC.
139
+ * @param {Date | string | null | undefined} value - Value to normalize.
140
+ * @returns {Date | string | null | undefined} - Normalized value.
141
+ */
142
+ export function normalizeDateValueForWrite(value) {
143
+ if (typeof value != "string") return value
144
+
145
+ return normalizeDateStringForWrite(value)
146
+ }
147
+
148
+ /**
149
+ * Normalizes a database value into a Date for record reads.
150
+ * SQLite timezone-less rows are legacy local wall-clock rows produced before
151
+ * UTC storage. New SQLite writes include `Z`, so they take the exact branch.
152
+ * @param {Date | string | null | undefined} value - Stored database value.
153
+ * @param {object} args - Options.
154
+ * @param {string} args.databaseType - Database driver type.
155
+ * @returns {Date | string | null | undefined} - Normalized value.
156
+ */
157
+ export function normalizeDateValueForRead(value, {databaseType}) {
158
+ if (value === null || value === undefined) return value
159
+
160
+ if (isDate(value)) return new Date(value.getTime())
161
+ if (typeof value != "string") return value
162
+ if (hasDateTimeTimezone(value)) return parseTimezoneQualifiedDateTimeString(value)
163
+ if (databaseType == "sqlite") return parseTimezoneLessDateTimeStringAsLocal(value)
164
+
165
+ return parseTimezoneLessDateTimeStringAsUtc(value)
166
+ }
167
+
168
+ /**
169
+ * Converts a legacy timezone-less datetime value into the new UTC database storage format.
170
+ * The optional offset follows JavaScript's `Date#getTimezoneOffset()` sign convention.
171
+ * @param {Date | string | null | undefined} value - Legacy value.
172
+ * @param {object} args - Options.
173
+ * @param {string} args.databaseType - Database driver type.
174
+ * @param {number | undefined} [args.legacyLocalOffsetMinutes] - UTC-minus-local offset in minutes.
175
+ * @returns {Date | string | null | undefined} - Converted database value or the original value.
176
+ */
177
+ export function convertLegacyDateValueToUtcStorage(value, {databaseType, legacyLocalOffsetMinutes}) {
178
+ if (typeof value != "string") return value
179
+ if (hasDateTimeTimezone(value)) return value
180
+
181
+ const parsedDate = legacyLocalOffsetMinutes === undefined
182
+ ? parseTimezoneLessDateTimeStringAsLocal(value)
183
+ : parseTimezoneLessDateTimeStringWithOffset(value, legacyLocalOffsetMinutes)
184
+
185
+ if (!isDate(parsedDate)) return value
186
+
187
+ return formatDateForDatabase(parsedDate, {databaseType})
188
+ }
@@ -103,12 +103,12 @@
103
103
 
104
104
  import BacktraceCleaner from "../../utils/backtrace-cleaner.js"
105
105
  import { getDatabaseAnnotations } from "../annotations.js"
106
+ import { formatDateForDatabase } from "../datetime-storage.js"
106
107
  import isDate from "../../utils/is-date.js"
107
108
  import Logger from "../../logger.js"
108
109
  import Query from "../query/index.js"
109
110
  import Handler from "../handler.js"
110
111
  import Mutex from "epic-locks/build/mutex.js"
111
- import strftime from "strftime"
112
112
  import UUID from "pure-uuid"
113
113
  import TableData from "../table-data/index.js"
114
114
  import TableColumn from "../table-data/table-column.js"
@@ -656,7 +656,7 @@ export default class VelociousDatabaseDriversBase {
656
656
  // isDate instead of instanceof: a Date created in another realm (e.g. the console REPL) would
657
657
  // fail instanceof, skip this conversion, and serialize as an empty SQL value downstream.
658
658
  if (isDate(value)) {
659
- return strftime("%F %T.%L", value)
659
+ return formatDateForDatabase(value, {databaseType: this.getType()})
660
660
  }
661
661
 
662
662
  // JSON-encode plain objects/arrays so they land in JSON/text columns as valid
@@ -30,6 +30,10 @@ export default class VelociousDatabaseDriversMssql extends Base{
30
30
  try {
31
31
  if (this.connection) await this.close()
32
32
 
33
+ if (sqlConfig) {
34
+ sqlConfig.options = Object.assign({}, sqlConfig.options, {useUTC: true})
35
+ }
36
+
33
37
  if (sqlConfig?.server && !sqlConfig.options?.serverName && net.isIP(sqlConfig.server)) {
34
38
  sqlConfig.options = Object.assign({}, sqlConfig.options, {serverName: ""})
35
39
  }
@@ -37,6 +37,8 @@ export default class VelociousDatabaseDriversMysql extends Base{
37
37
  async connect() {
38
38
  this.pool = mysql.createPool(Object.assign({connectionLimit: 1}, this.connectArgs()))
39
39
  this.pool.on("error", this.onPoolError)
40
+
41
+ await this.setSessionTimezoneToUtc()
40
42
  }
41
43
 
42
44
  /**
@@ -75,6 +77,14 @@ export default class VelociousDatabaseDriversMysql extends Base{
75
77
  await super.clearConnectionCheckoutName()
76
78
  }
77
79
 
80
+ /**
81
+ * Sets the database session timezone to UTC so bare timestamp literals store UTC instants.
82
+ * @returns {Promise<void>} - Resolves when complete.
83
+ */
84
+ async setSessionTimezoneToUtc() {
85
+ await this.query("SET time_zone = '+00:00'", {logName: "Set Session Time Zone", processListComment: false})
86
+ }
87
+
78
88
  /**
79
89
  * Runs connect args.
80
90
  * @returns {Record<string, ?>} - The connect args.
@@ -86,7 +96,7 @@ export default class VelociousDatabaseDriversMysql extends Base{
86
96
  /**
87
97
  * Connect args.
88
98
  * @type {Record<string, ?>} */
89
- const connectArgs = {charset: "utf8mb4"}
99
+ const connectArgs = {charset: "utf8mb4", timezone: "Z"}
90
100
 
91
101
  for (const forwardValue of forward) {
92
102
  if (forwardValue in args) connectArgs[forwardValue] = digg(args, forwardValue)
@@ -3,7 +3,7 @@
3
3
  import AlterTable from "./sql/alter-table.js"
4
4
  import wait from "awaitery/build/wait.js"
5
5
  import Base from "../base.js"
6
- import {Client} from "pg"
6
+ import { Client, types as pgTypes } from "pg"
7
7
  import CreateDatabase from "./sql/create-database.js"
8
8
  import CreateIndex from "./sql/create-index.js"
9
9
  import CreateTable from "./sql/create-table.js"
@@ -19,12 +19,18 @@ import StructureSql from "./structure-sql.js"
19
19
  import Upsert from "./sql/upsert.js"
20
20
  import Update from "./sql/update.js"
21
21
 
22
+ const PG_TIMESTAMP_WITHOUT_TIMEZONE_OID = 1114
23
+
24
+ pgTypes.setTypeParser(PG_TIMESTAMP_WITHOUT_TIMEZONE_OID, (value) => new Date(`${value.replace(" ", "T")}Z`))
25
+
22
26
  export default class VelociousDatabaseDriversPgsql extends Base{
23
27
  async connect() {
24
28
  const client = new Client(this.connectArgs())
25
29
 
26
30
  try {
27
31
  await client.connect()
32
+ this.connection = client
33
+ await this.setSessionTimezoneToUtc()
28
34
  } catch (error) {
29
35
  // Re-throw to recover real stack trace
30
36
  if (error instanceof Error) {
@@ -33,8 +39,6 @@ export default class VelociousDatabaseDriversPgsql extends Base{
33
39
  throw new Error(`Connect to Postgres server failed: ${error}`, {cause: error})
34
40
  }
35
41
  }
36
-
37
- this.connection = client
38
42
  }
39
43
 
40
44
  connectArgs() {
@@ -85,6 +89,14 @@ export default class VelociousDatabaseDriversPgsql extends Base{
85
89
  await super.clearConnectionCheckoutName()
86
90
  }
87
91
 
92
+ /**
93
+ * Sets the database session timezone to UTC so bare timestamp literals store UTC instants.
94
+ * @returns {Promise<void>} - Resolves when complete.
95
+ */
96
+ async setSessionTimezoneToUtc() {
97
+ await this.query("SET TIME ZONE 'UTC'", {logName: "Set Session Time Zone", processListComment: false})
98
+ }
99
+
88
100
  /**
89
101
  * Runs alter table sqls.
90
102
  * @param {import("../../table-data/index.js").default} tableData - Table data.
@@ -26,7 +26,15 @@
26
26
  * CreateTableCallbackType type.
27
27
  * @typedef {(table: TableData) => void} CreateTableCallbackType
28
28
  */
29
+ /**
30
+ * LegacyLocalDateTimesMigrationArgsType type.
31
+ * @typedef {object} LegacyLocalDateTimesMigrationArgsType
32
+ * @property {Record<string, string[]>} [columnsByTable] - Explicit datetime columns keyed by table name.
33
+ * @property {number} [legacyLocalOffsetMinutes] - UTC-minus-local offset in minutes for legacy rows.
34
+ * @property {string[]} [tables] - Tables to migrate. Defaults to all non-internal tables.
35
+ */
29
36
 
37
+ import { convertLegacyDateValueToUtcStorage } from "../datetime-storage.js"
30
38
  import * as inflection from "inflection"
31
39
  import restArgsError from "../../utils/rest-args-error.js"
32
40
  import TableData from "../table-data/index.js"
@@ -275,6 +283,125 @@ export default class VelociousDatabaseMigration {
275
283
  await column.changeNullable(nullable)
276
284
  }
277
285
 
286
+ /**
287
+ * Migrates legacy timezone-less local datetime rows into UTC datetime storage.
288
+ * New SQLite UTC rows include a timezone suffix and are skipped.
289
+ * @param {LegacyLocalDateTimesMigrationArgsType} [args] - Migration options.
290
+ * @returns {Promise<void>} - Resolves when complete.
291
+ */
292
+ async migrateLegacyLocalDateTimesToUtcStorage(args = {}) {
293
+ const {columnsByTable, legacyLocalOffsetMinutes, tables, ...restArgs} = args
294
+
295
+ restArgsError(restArgs)
296
+
297
+ const tableNames = await this._legacyLocalDateTimesTableNames(tables)
298
+
299
+ for (const tableName of tableNames) {
300
+ await this._migrateLegacyLocalDateTimesTable({
301
+ columnsByTable,
302
+ legacyLocalOffsetMinutes,
303
+ tableName
304
+ })
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Resolves table names for a legacy local datetime migration.
310
+ * @param {string[] | undefined} tables - Explicit table names.
311
+ * @returns {Promise<string[]>} - Table names.
312
+ */
313
+ async _legacyLocalDateTimesTableNames(tables) {
314
+ if (tables) return tables
315
+
316
+ return (await this.getDriver().getTables())
317
+ .map((table) => table.getName())
318
+ .filter((tableName) => tableName != "schema_migrations" && !tableName.startsWith("sqlite_"))
319
+ }
320
+
321
+ /**
322
+ * Migrates one table's legacy local datetime values.
323
+ * @param {object} args - Options.
324
+ * @param {Record<string, string[]> | undefined} args.columnsByTable - Explicit columns keyed by table.
325
+ * @param {number | undefined} args.legacyLocalOffsetMinutes - UTC-minus-local offset in minutes.
326
+ * @param {string} args.tableName - Table name.
327
+ * @returns {Promise<void>} - Resolves when complete.
328
+ */
329
+ async _migrateLegacyLocalDateTimesTable({columnsByTable, legacyLocalOffsetMinutes, tableName}) {
330
+ const driver = this.getDriver()
331
+ const table = await driver.getTableByNameOrFail(tableName)
332
+ const columns = await this._legacyLocalDateTimesColumns({columnsByTable, table})
333
+
334
+ if (columns.length === 0) return
335
+
336
+ const primaryKeyColumn = await this._legacyLocalDateTimesPrimaryKey(table)
337
+ const selectedColumns = [primaryKeyColumn, ...columns]
338
+ const selectSql = selectedColumns
339
+ .map((columnName) => driver.quoteColumn(columnName))
340
+ .join(", ")
341
+ const rows = await driver.query(`SELECT ${selectSql} FROM ${driver.quoteTable(tableName)}`)
342
+
343
+ for (const row of rows) {
344
+ for (const columnName of columns) {
345
+ const value = row[columnName]
346
+ const convertedValue = convertLegacyDateValueToUtcStorage(value, {
347
+ databaseType: driver.getType(),
348
+ legacyLocalOffsetMinutes
349
+ })
350
+
351
+ if (convertedValue === value) continue
352
+
353
+ await driver.query(`
354
+ UPDATE ${driver.quoteTable(tableName)}
355
+ SET ${driver.quoteColumn(columnName)} = ${driver.quote(convertedValue)}
356
+ WHERE ${driver.quoteColumn(primaryKeyColumn)} = ${driver.quote(row[primaryKeyColumn])}
357
+ `)
358
+ }
359
+ }
360
+ }
361
+
362
+ /**
363
+ * Resolves date-like columns for one table.
364
+ * @param {object} args - Options.
365
+ * @param {Record<string, string[]> | undefined} args.columnsByTable - Explicit columns keyed by table.
366
+ * @param {import("../drivers/base-table.js").default} args.table - Table metadata.
367
+ * @returns {Promise<string[]>} - Date-like column names.
368
+ */
369
+ async _legacyLocalDateTimesColumns({columnsByTable, table}) {
370
+ const explicitColumns = columnsByTable?.[table.getName()]
371
+
372
+ if (explicitColumns) return explicitColumns
373
+
374
+ return (await table.getColumns())
375
+ .filter((column) => this._legacyLocalDateTimesColumnIsDateLike(column))
376
+ .map((column) => column.getName())
377
+ }
378
+
379
+ /**
380
+ * Checks whether a column should be included by default.
381
+ * @param {import("../drivers/base-column.js").default} column - Column metadata.
382
+ * @returns {boolean} - Whether the column is date-like.
383
+ */
384
+ _legacyLocalDateTimesColumnIsDateLike(column) {
385
+ const columnType = column.getType().toLowerCase()
386
+
387
+ return columnType.includes("date") || columnType.includes("timestamp")
388
+ }
389
+
390
+ /**
391
+ * Resolves the single primary key column for row updates.
392
+ * @param {import("../drivers/base-table.js").default} table - Table metadata.
393
+ * @returns {Promise<string>} - Primary key column name.
394
+ */
395
+ async _legacyLocalDateTimesPrimaryKey(table) {
396
+ const primaryKeyColumns = (await table.getColumns()).filter((column) => column.getPrimaryKey())
397
+
398
+ if (primaryKeyColumns.length != 1) {
399
+ throw new Error(`Expected exactly one primary key on ${table.getName()} but found ${primaryKeyColumns.length}`)
400
+ }
401
+
402
+ return primaryKeyColumns[0].getName()
403
+ }
404
+
278
405
  /**
279
406
  * Runs column exists.
280
407
  * @param {string} tableName - Table name.
@@ -31,13 +31,13 @@ import HasOneRelationship from "./relationships/has-one.js"
31
31
  import RecordAttachmentHandle from "./attachments/handle.js"
32
32
  import * as inflection from "inflection"
33
33
  import deburrColumnName from "../../utils/deburr-column-name.js"
34
- import isDate from "../../utils/is-date.js"
35
34
  import ModelClassQuery from "../query/model-class-query.js"
36
35
  import Preloader from "../query/preloader.js"
37
36
  import {readPayloadAssociationCount, readPayloadComputedAbility, readPayloadQueryData, setPayloadAssociationCount, setPayloadComputedAbility, setPayloadQueryData} from "../../record-payload-values.js"
38
37
  import restArgsError from "../../utils/rest-args-error.js"
39
38
  import singularizeModelName from "../../utils/singularize-model-name.js"
40
39
  import {defineModelScope} from "../../utils/model-scope.js"
40
+ import { normalizeDateStringForWrite, normalizeDateValueForRead, normalizeDateValueForWrite } from "../datetime-storage.js"
41
41
  import {formatValue} from "../../utils/format-value.js"
42
42
  import ValidatorsFormat from "./validators/format.js"
43
43
  import ValidatorsPresence from "./validators/presence.js"
@@ -1866,17 +1866,7 @@ class VelociousDatabaseRecord {
1866
1866
  * @returns {?} - The date value.
1867
1867
  */
1868
1868
  _normalizeDateValue(value) {
1869
- if (typeof value != "string") return value
1870
-
1871
- const isoDateTimeRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:Z|[+-]\d{2}:\d{2})$/
1872
-
1873
- if (!isoDateTimeRegex.test(value)) return value
1874
-
1875
- const timestamp = Date.parse(value)
1876
-
1877
- if (Number.isNaN(timestamp)) return value
1878
-
1879
- return new Date(timestamp)
1869
+ return normalizeDateValueForWrite(value)
1880
1870
  }
1881
1871
 
1882
1872
  /**
@@ -2240,21 +2230,7 @@ class VelociousDatabaseRecord {
2240
2230
  * @returns {?} - Normalized value.
2241
2231
  */
2242
2232
  static _normalizeDateValueForInsert(value) {
2243
- let normalizedValue = value
2244
-
2245
- if (typeof normalizedValue == "string") {
2246
- normalizedValue = this._normalizeDateStringForInsert(normalizedValue)
2247
- }
2248
-
2249
- if (isDate(normalizedValue)) {
2250
- const configuration = this._getConfiguration()
2251
- const offsetMinutes = configuration.getEnvironmentHandler().getTimezoneOffsetMinutes(configuration)
2252
- const offsetMs = offsetMinutes * 60 * 1000
2253
-
2254
- normalizedValue = new Date(normalizedValue.getTime() - offsetMs)
2255
- }
2256
-
2257
- return normalizedValue
2233
+ return normalizeDateValueForWrite(value)
2258
2234
  }
2259
2235
 
2260
2236
  /**
@@ -2263,15 +2239,7 @@ class VelociousDatabaseRecord {
2263
2239
  * @returns {string | Date} - Parsed date or original string.
2264
2240
  */
2265
2241
  static _normalizeDateStringForInsert(value) {
2266
- const isoDateTimeRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:Z|[+-]\d{2}:\d{2})$/
2267
-
2268
- if (!isoDateTimeRegex.test(value)) return value
2269
-
2270
- const timestamp = Date.parse(value)
2271
-
2272
- if (Number.isNaN(timestamp)) return value
2273
-
2274
- return new Date(timestamp)
2242
+ return normalizeDateStringForWrite(value)
2275
2243
  }
2276
2244
 
2277
2245
  /**
@@ -3925,26 +3893,7 @@ class VelociousDatabaseRecord {
3925
3893
  * @returns {?} - Normalized value.
3926
3894
  */
3927
3895
  _normalizeDateValueForRead(value) {
3928
- if (value === null || value === undefined) return value
3929
-
3930
- const configuration = this.getModelClass()._getConfiguration()
3931
- const offsetMinutes = configuration.getEnvironmentHandler().getTimezoneOffsetMinutes(configuration)
3932
- const offsetMs = offsetMinutes * 60 * 1000
3933
-
3934
- if (isDate(value)) {
3935
- return new Date(value.getTime() + offsetMs)
3936
- }
3937
-
3938
- if (typeof value != "string") return value
3939
-
3940
- const hasTimezone = /[zZ]|[+-]\d{2}:\d{2}$/.test(value)
3941
- const normalized = value.includes("T") ? value : value.replace(" ", "T")
3942
- const parseValue = hasTimezone ? normalized : `${normalized}Z`
3943
- const parsed = Date.parse(parseValue)
3944
-
3945
- if (Number.isNaN(parsed)) return value
3946
-
3947
- return new Date(parsed + offsetMs)
3896
+ return normalizeDateValueForRead(value, {databaseType: this.getModelClass().getDatabaseType()})
3948
3897
  }
3949
3898
 
3950
3899
  _belongsToChanges() {
@@ -4079,10 +4028,6 @@ class VelociousDatabaseRecord {
4079
4028
  * @returns {void} - No return value.
4080
4029
  */
4081
4030
  _normalizeDateValuesForWrite(data) {
4082
- const configuration = this.getModelClass()._getConfiguration()
4083
- const offsetMinutes = configuration.getEnvironmentHandler().getTimezoneOffsetMinutes(configuration)
4084
- const offsetMs = offsetMinutes * 60 * 1000
4085
-
4086
4031
  for (const columnName in data) {
4087
4032
  const columnType = this.getModelClass().getColumnTypeByName(columnName)
4088
4033
 
@@ -4090,9 +4035,7 @@ class VelociousDatabaseRecord {
4090
4035
 
4091
4036
  const value = data[columnName]
4092
4037
 
4093
- if (!isDate(value)) continue
4094
-
4095
- data[columnName] = new Date(value.getTime() - offsetMs)
4038
+ data[columnName] = normalizeDateValueForWrite(value)
4096
4039
  }
4097
4040
  }
4098
4041
 
@@ -10,7 +10,9 @@ import {normalizeGroup as normalizeQueryGroup, normalizeJoins as normalizeQueryJ
10
10
  import {assignSafeProperty, deserializeFrontendModelTransportValue, isBackendModelInstance, serializeFrontendModelTransportValue} from "./frontend-models/transport-serialization.js"
11
11
  import RoutesResolver from "./routes/resolver.js"
12
12
  import {ValidationError} from "./database/record/index.js"
13
+ import { normalizeDateStringForWrite } from "./database/datetime-storage.js"
13
14
  import VelociousError from "./velocious-error.js"
15
+ import isDate from "./utils/is-date.js"
14
16
  import isPlainObject from "./utils/plain-object.js"
15
17
 
16
18
  /**
@@ -2212,9 +2214,9 @@ export default class FrontendModelController extends Controller {
2212
2214
  const isDateTimeColumn = typeof columnType === "string" && ["date", "datetime", "timestamp"].some((type) => columnType.includes(type))
2213
2215
 
2214
2216
  if (isDateTimeColumn) {
2215
- const parsedDate = new Date(value)
2217
+ const parsedDate = normalizeDateStringForWrite(value)
2216
2218
 
2217
- if (!Number.isNaN(parsedDate.getTime())) {
2219
+ if (isDate(parsedDate)) {
2218
2220
  return parsedDate
2219
2221
  }
2220
2222
  }
@@ -5,6 +5,7 @@ import timeout from "awaitery/build/timeout.js"
5
5
  import wait from "awaitery/build/wait.js"
6
6
  import FrontendModelQuery, {frontendModelEventOptionsPayload} from "./query.js"
7
7
  import FrontendModelPreloader from "./preloader.js"
8
+ import { normalizeDateStringForWrite } from "../database/datetime-storage.js"
8
9
  import {registerFrontendModel, resolveFrontendModelClass} from "./model-registry.js"
9
10
  import {validateFrontendModelResourceCommandName, validateFrontendModelResourcePath} from "./resource-config-validation.js"
10
11
  import {deserializeFrontendModelTransportValue, serializeFrontendModelTransportValue} from "./transport-serialization.js"
@@ -3775,6 +3776,12 @@ export default class FrontendModelBase {
3775
3776
  */
3776
3777
  static findByPrimitiveValuesMatch(actualValue, expectedValue) {
3777
3778
  if (actualValue instanceof Date && typeof expectedValue === "string") {
3779
+ const normalizedExpectedValue = normalizeDateStringForWrite(expectedValue)
3780
+
3781
+ if (normalizedExpectedValue instanceof Date) {
3782
+ return actualValue.toISOString() === normalizedExpectedValue.toISOString()
3783
+ }
3784
+
3778
3785
  return actualValue.toISOString() === expectedValue
3779
3786
  }
3780
3787
 
@@ -1 +1 @@
1
- {"version":3,"file":"drop.d.ts","sourceRoot":"","sources":["../../../../../src/cli/commands/db/drop.js"],"names":[],"mappings":"AAIA;IACE;;;OAGG;IACH,WAFa,OAAO,CAAC,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CA0CzC;IAED;;;;OAIG;IACH,yCAHW,MAAM,GACJ,MAAM,CAOlB;IAED;;;;OAIG;IACH,iCAHW,MAAM,GACJ,OAAO,CAAC,IAAI,CAAC,CAOzB;CACF;0BA1EyB,mBAAmB"}
1
+ {"version":3,"file":"drop.d.ts","sourceRoot":"","sources":["../../../../../src/cli/commands/db/drop.js"],"names":[],"mappings":"AAIA;IACE;;;OAGG;IACH,WAFa,OAAO,CAAC,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CA8CzC;IAED;;;;OAIG;IACH,yCAHW,MAAM,GACJ,MAAM,CAOlB;IAED;;;;OAIG;IACH,iCAHW,MAAM,GACJ,OAAO,CAAC,IAAI,CAAC,CAOzB;CACF;0BA9EyB,mBAAmB"}