velocious 1.0.470 → 1.0.471

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 (74) hide show
  1. package/README.md +4 -2
  2. package/build/configuration-types.js +1 -0
  3. package/build/configuration.js +27 -1
  4. package/build/controller.js +13 -1
  5. package/build/database/datetime-storage.js +49 -5
  6. package/build/database/record/index.js +14 -4
  7. package/build/environment-handlers/base.js +46 -0
  8. package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +362 -38
  9. package/build/environment-handlers/node.js +84 -8
  10. package/build/frontend-model-controller.js +7 -7
  11. package/build/frontend-models/base.js +75 -9
  12. package/build/frontend-models/transport-serialization.js +41 -8
  13. package/build/frontend-models/websocket-channel.js +18 -1
  14. package/build/frontend-models/websocket-publishers.js +12 -1
  15. package/build/http-server/client/request-runner.js +13 -1
  16. package/build/src/configuration-types.d.ts +5 -0
  17. package/build/src/configuration-types.d.ts.map +1 -1
  18. package/build/src/configuration-types.js +2 -1
  19. package/build/src/configuration.d.ts +14 -1
  20. package/build/src/configuration.d.ts.map +1 -1
  21. package/build/src/configuration.js +25 -2
  22. package/build/src/controller.d.ts +5 -0
  23. package/build/src/controller.d.ts.map +1 -1
  24. package/build/src/controller.js +12 -2
  25. package/build/src/database/datetime-storage.d.ts +12 -4
  26. package/build/src/database/datetime-storage.d.ts.map +1 -1
  27. package/build/src/database/datetime-storage.js +48 -6
  28. package/build/src/database/record/index.d.ts +5 -0
  29. package/build/src/database/record/index.d.ts.map +1 -1
  30. package/build/src/database/record/index.js +13 -5
  31. package/build/src/environment-handlers/base.d.ts +19 -0
  32. package/build/src/environment-handlers/base.d.ts.map +1 -1
  33. package/build/src/environment-handlers/base.js +41 -1
  34. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts +219 -40
  35. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts.map +1 -1
  36. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +312 -39
  37. package/build/src/environment-handlers/node.d.ts +1 -0
  38. package/build/src/environment-handlers/node.d.ts.map +1 -1
  39. package/build/src/environment-handlers/node.js +76 -9
  40. package/build/src/frontend-model-controller.js +8 -8
  41. package/build/src/frontend-models/base.d.ts +4 -0
  42. package/build/src/frontend-models/base.d.ts.map +1 -1
  43. package/build/src/frontend-models/base.js +64 -9
  44. package/build/src/frontend-models/transport-serialization.d.ts +30 -1
  45. package/build/src/frontend-models/transport-serialization.d.ts.map +1 -1
  46. package/build/src/frontend-models/transport-serialization.js +34 -9
  47. package/build/src/frontend-models/websocket-channel.d.ts.map +1 -1
  48. package/build/src/frontend-models/websocket-channel.js +16 -2
  49. package/build/src/frontend-models/websocket-publishers.d.ts.map +1 -1
  50. package/build/src/frontend-models/websocket-publishers.js +12 -2
  51. package/build/src/http-server/client/request-runner.d.ts.map +1 -1
  52. package/build/src/http-server/client/request-runner.js +13 -2
  53. package/build/src/time-zone.d.ts +16 -0
  54. package/build/src/time-zone.d.ts.map +1 -0
  55. package/build/src/time-zone.js +56 -0
  56. package/build/time-zone.js +65 -0
  57. package/build/tsconfig.tsbuildinfo +1 -1
  58. package/package.json +2 -1
  59. package/scripts/test-browser.js +1 -0
  60. package/src/configuration-types.js +1 -0
  61. package/src/configuration.js +27 -1
  62. package/src/controller.js +13 -1
  63. package/src/database/datetime-storage.js +49 -5
  64. package/src/database/record/index.js +14 -4
  65. package/src/environment-handlers/base.js +46 -0
  66. package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +362 -38
  67. package/src/environment-handlers/node.js +84 -8
  68. package/src/frontend-model-controller.js +7 -7
  69. package/src/frontend-models/base.js +75 -9
  70. package/src/frontend-models/transport-serialization.js +41 -8
  71. package/src/frontend-models/websocket-channel.js +18 -1
  72. package/src/frontend-models/websocket-publishers.js +12 -1
  73. package/src/http-server/client/request-runner.js +13 -1
  74. package/src/time-zone.js +65 -0
package/README.md CHANGED
@@ -378,7 +378,7 @@ export default new Configuration({
378
378
  `frontendModels` entries must be `FrontendModelBaseResource` subclasses. Built-in CRUD/find/index/serialize behavior lives in the base class, and app resources override only the pieces they actually need.
379
379
  Resource-level index customization should prefer `indexQuery()` or the pagination/search/sort hooks over replacing `records()`, so built-in pluck and aggregate count support can keep using the same query. See [`docs/frontend-model-resources.md`](docs/frontend-model-resources.md) for the resource extension points.
380
380
 
381
- Custom class- and instance-level commands are declared via `collectionCommands` / `memberCommands`. Each entry is a plain camelCase method name, or a `{name, args?, returnType?}` object that types the command's arguments and response — e.g. `memberCommands: ["suspend", {name: "refresh", args: [{name: "age", type: "number"}], returnType: "string"}]`. A command whose args are a single object literal with only optional fields generates an omittable parameter (`record.command()` works without passing `{}`); any required field keeps the argument mandatory. See [`docs/frontend-model-resources.md#custom-commands`](docs/frontend-model-resources.md#custom-commands).
381
+ Custom class- and instance-level commands are declared via `collectionCommands` / `memberCommands`. Each entry is a plain camelCase method name, or a `{name, args?, returnType?}` object that types the command's arguments and response — e.g. `memberCommands: ["suspend", {name: "refresh", args: [{name: "age", type: "number"}], returnType: "string"}]`. Plain string commands derive args and return types from the backend resource method JSDoc; shared DTO `@import` types outside the backend `src` tree are preserved in generated frontend models, while backend-local helper types such as `ReturnType<typeof serializePayload>` are rejected. A command whose args are a single object literal with only optional fields generates an omittable parameter (`record.command()` works without passing `{}`); any required field keeps the argument mandatory. See [`docs/frontend-model-resources.md#custom-commands`](docs/frontend-model-resources.md#custom-commands).
382
382
 
383
383
  Resources expose the full CRUD ability set (`create`, `destroy`, `read`, `update`) by default. To restrict the API surface — for example to a read-only resource — declare an explicit subset:
384
384
 
@@ -565,13 +565,15 @@ When your frontend app calls a backend on another host/port (or under a path pre
565
565
  import FrontendModelBase from "velocious/build/src/frontend-models/base.js"
566
566
 
567
567
  FrontendModelBase.configureTransport({
568
- url: "http://127.0.0.1:4501/frontend-models"
568
+ url: "http://127.0.0.1:4501/frontend-models",
569
+ timeZone: () => Intl.DateTimeFormat().resolvedOptions().timeZone
569
570
  })
570
571
  ```
571
572
 
572
573
  Available transport options:
573
574
 
574
575
  - `url` (can also be a relative path like `"/frontend-models"` on web)
576
+ - `timeZone` (an IANA timezone string or a function returning one). Browser clients auto-detect this when it is not configured. Frontend-model datetime strings without an explicit timezone are interpreted in this request timezone and stored/queried as UTC instants.
575
577
 
576
578
  Use `await FrontendModelBase.waitForIdle()` when a test harness or app lifecycle needs to wait for queued, scheduled, and active frontend-model transport requests to finish before resetting state.
577
579
 
@@ -474,6 +474,7 @@
474
474
  * @property {TenantDatabaseResolverType} [tenantDatabaseResolver] - Resolver for deriving tenant-specific database config overrides.
475
475
  * @property {Record<string, TenantDatabaseProviderType>} [tenantDatabaseProviders] - Tenant database lifecycle providers keyed by database identifier.
476
476
  * @property {string} [testing] - Path to the testing configuration file.
477
+ * @property {string | (() => string | undefined)} [timeZone] - Default timezone for timezone-less datetime strings.
477
478
  * @property {number | (() => number)} [timezoneOffsetMinutes] - Default timezone offset in minutes.
478
479
  * @property {string | string[]} [trustedProxies] - Trusted reverse proxy address ranges used to resolve request remote addresses from forwarding headers.
479
480
  * @property {number | (() => number)} [requestTimeoutMs] - Timeout in seconds for completing a HTTP request.
@@ -22,6 +22,7 @@ import {requestDetails} from "./error-reporting/request-details.js"
22
22
  import {frontendModelResourceClassFromDefinition, frontendModelResourceConfigurationFromDefinition, frontendModelResourcesForBackendProject} from "./frontend-models/resource-definition.js"
23
23
  import PluginRoutes from "./routes/plugin-routes.js"
24
24
  import restArgsError from "./utils/rest-args-error.js"
25
+ import {validateTimeZone} from "./time-zone.js"
25
26
  import {withTrackedStack} from "./utils/with-tracked-stack.js"
26
27
 
27
28
  export {CurrentConfigurationNotSetError}
@@ -125,7 +126,7 @@ export default class VelociousConfiguration {
125
126
  * Runs constructor.
126
127
  * @param {import("./configuration-types.js").ConfigurationArgsType} args - Configuration arguments.
127
128
  */
128
- constructor({abilityResolver, abilityResources, attachments, autoload = true, backgroundJobs, backendProjects, beacon, cookieSecret, cors, database, debug = false, debugEndpoint = false, directory, enforceTenantDatabaseScopes = true, environment, environmentHandler, exposeInternalErrorsToClients = false, httpServer, initializeModels, initializers, locale, localeFallbacks, locales, logging, mailerBackend, requestTimeoutMs, routeResolverHooks, scheduledBackgroundJobs, structureSql, tenantDatabaseProviders, tenantDatabaseResolver, tenantResolver, testing, timezoneOffsetMinutes, trustedProxies, websocketChannelResolver, websocketMessageHandlerResolver, ...restArgs}) {
129
+ constructor({abilityResolver, abilityResources, attachments, autoload = true, backgroundJobs, backendProjects, beacon, cookieSecret, cors, database, debug = false, debugEndpoint = false, directory, enforceTenantDatabaseScopes = true, environment, environmentHandler, exposeInternalErrorsToClients = false, httpServer, initializeModels, initializers, locale, localeFallbacks, locales, logging, mailerBackend, requestTimeoutMs, routeResolverHooks, scheduledBackgroundJobs, structureSql, tenantDatabaseProviders, tenantDatabaseResolver, tenantResolver, testing, timeZone, timezoneOffsetMinutes, trustedProxies, websocketChannelResolver, websocketMessageHandlerResolver, ...restArgs}) {
129
130
  restArgsError(restArgs)
130
131
 
131
132
  this._abilityResolver = abilityResolver
@@ -183,6 +184,7 @@ export default class VelociousConfiguration {
183
184
  this.locales = locales
184
185
  this._initializers = initializers
185
186
  this._testing = testing
187
+ this._timeZone = timeZone
186
188
  this._timezoneOffsetMinutes = timezoneOffsetMinutes
187
189
  this._trustedProxies = trustedProxies
188
190
  this._requestTimeoutMs = requestTimeoutMs
@@ -1848,6 +1850,20 @@ export default class VelociousConfiguration {
1848
1850
  return new Date().getTimezoneOffset()
1849
1851
  }
1850
1852
 
1853
+ /**
1854
+ * Runs get time zone.
1855
+ * @returns {string | undefined} - Configured timezone identifier.
1856
+ */
1857
+ getTimeZone() {
1858
+ const timeZone = typeof this._timeZone === "function"
1859
+ ? this._timeZone()
1860
+ : this._timeZone
1861
+
1862
+ if (timeZone === undefined || timeZone === null) return undefined
1863
+
1864
+ return validateTimeZone(timeZone, "configuration timeZone")
1865
+ }
1866
+
1851
1867
  /**
1852
1868
  * Runs get websocket events.
1853
1869
  * @returns {import("./http-server/websocket-events.js").default | undefined} - The websocket events.
@@ -2330,6 +2346,16 @@ export default class VelociousConfiguration {
2330
2346
  return await this.getEnvironmentHandler().runWithRequestTiming(requestTiming, callback)
2331
2347
  }
2332
2348
 
2349
+ /**
2350
+ * Runs run with timezone.
2351
+ * @param {string} timeZone - IANA timezone identifier.
2352
+ * @param {() => Promise<?>} callback - Callback.
2353
+ * @returns {Promise<?>} - Callback result.
2354
+ */
2355
+ async runWithTimezone(timeZone, callback) {
2356
+ return await this.getEnvironmentHandler().runWithTimezone(timeZone, callback)
2357
+ }
2358
+
2333
2359
  /**
2334
2360
  * Runs get current ability.
2335
2361
  * @returns {import("./authorization/ability.js").default | undefined} - Current ability from context.
@@ -83,6 +83,18 @@ export default class VelociousController {
83
83
  */
84
84
  getRequest() { return this._request }
85
85
 
86
+ /**
87
+ * Runs transport serialization options.
88
+ * @returns {import("./frontend-models/transport-serialization.js").FrontendModelTransportSerializationOptions} - Serialization options.
89
+ */
90
+ transportSerializationOptions() {
91
+ const configuration = this.getConfiguration()
92
+
93
+ return {
94
+ timeZone: configuration.getEnvironmentHandler().getTimeZone(configuration)
95
+ }
96
+ }
97
+
86
98
  /**
87
99
  * Runs set cookie.
88
100
  * @param {string} name - Cookie name.
@@ -257,7 +269,7 @@ export default class VelociousController {
257
269
  */
258
270
  renderJsonArg(json) {
259
271
  return this._measureViewRender(() => {
260
- const body = JSON.stringify(serializeFrontendModelTransportValue(json))
272
+ const body = JSON.stringify(serializeFrontendModelTransportValue(json, this.transportSerializationOptions()))
261
273
 
262
274
  this._response.setHeader("Content-Type", "application/json; charset=UTF-8")
263
275
  this._response.setBody(body)
@@ -1,9 +1,12 @@
1
1
  // @ts-check
2
2
 
3
3
  import isDate from "../utils/is-date.js"
4
+ import {Temporal} from "@js-temporal/polyfill"
5
+ import {validateTimeZone} from "../time-zone.js"
4
6
 
5
7
  const dateTimeWithTimezonePattern = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:[zZ]|[+-]\d{2}:\d{2})$/
6
8
  const dateTimeWithoutTimezonePattern = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?$/
9
+ const dateTimeWithoutTimezonePartsPattern = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?$/
7
10
  const timezoneSuffixPattern = /(?:[zZ]|[+-]\d{2}:\d{2})$/
8
11
 
9
12
  /**
@@ -85,6 +88,42 @@ function parseTimezoneLessDateTimeStringWithOffset(value, legacyLocalOffsetMinut
85
88
  return new Date(utcDate.getTime() + (legacyLocalOffsetMinutes * 60 * 1000))
86
89
  }
87
90
 
91
+ /**
92
+ * Parses a timezone-less datetime string in a named timezone.
93
+ * @param {string} value - Datetime string.
94
+ * @param {string} timeZone - IANA timezone identifier.
95
+ * @returns {Date | string} - Parsed date or the original string when it is not a recognized datetime.
96
+ */
97
+ function parseTimezoneLessDateTimeStringWithTimeZone(value, timeZone) {
98
+ const match = value.match(dateTimeWithoutTimezonePartsPattern)
99
+
100
+ if (!match) return value
101
+
102
+ const normalizedTimeZone = validateTimeZone(timeZone, "timeZone")
103
+ const fraction = (match[7] || "").padEnd(9, "0")
104
+
105
+ try {
106
+ const zonedDateTime = Temporal.ZonedDateTime.from({
107
+ day: Number(match[3]),
108
+ hour: Number(match[4]),
109
+ microsecond: Number(fraction.slice(3, 6)),
110
+ millisecond: Number(fraction.slice(0, 3)),
111
+ minute: Number(match[5]),
112
+ month: Number(match[2]),
113
+ nanosecond: Number(fraction.slice(6, 9)),
114
+ second: Number(match[6]),
115
+ timeZone: normalizedTimeZone,
116
+ year: Number(match[1])
117
+ })
118
+
119
+ return new Date(Number(zonedDateTime.epochMilliseconds))
120
+ } catch (error) {
121
+ if (error instanceof RangeError) return value
122
+
123
+ throw error
124
+ }
125
+ }
126
+
88
127
  /**
89
128
  * Checks whether a string has a datetime timezone suffix.
90
129
  * @param {string} value - Value to check.
@@ -123,26 +162,31 @@ export function formatDateForDatabase(value, {databaseType}) {
123
162
 
124
163
  /**
125
164
  * 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.
165
+ * Timezone-less strings are interpreted in the given timezone when present, otherwise UTC.
127
166
  * @param {string} value - Value to normalize.
167
+ * @param {object} [options] - Parse options.
168
+ * @param {string | undefined} [options.timeZone] - Timezone for timezone-less strings.
128
169
  * @returns {Date | string} - Normalized value.
129
170
  */
130
- export function normalizeDateStringForWrite(value) {
171
+ export function normalizeDateStringForWrite(value, {timeZone} = {}) {
131
172
  if (hasDateTimeTimezone(value)) return parseTimezoneQualifiedDateTimeString(value)
173
+ if (timeZone !== undefined) return parseTimezoneLessDateTimeStringWithTimeZone(value, timeZone)
132
174
 
133
175
  return parseTimezoneLessDateTimeStringAsUtc(value)
134
176
  }
135
177
 
136
178
  /**
137
179
  * 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.
180
+ * Timezone-less strings are interpreted in the given timezone when present, otherwise UTC.
139
181
  * @param {Date | string | null | undefined} value - Value to normalize.
182
+ * @param {object} [options] - Parse options.
183
+ * @param {string | undefined} [options.timeZone] - Timezone for timezone-less strings.
140
184
  * @returns {Date | string | null | undefined} - Normalized value.
141
185
  */
142
- export function normalizeDateValueForWrite(value) {
186
+ export function normalizeDateValueForWrite(value, {timeZone} = {}) {
143
187
  if (typeof value != "string") return value
144
188
 
145
- return normalizeDateStringForWrite(value)
189
+ return normalizeDateStringForWrite(value, {timeZone})
146
190
  }
147
191
 
148
192
  /**
@@ -1800,7 +1800,7 @@ class VelociousDatabaseRecord {
1800
1800
  * @returns {?} - The date value.
1801
1801
  */
1802
1802
  _normalizeDateValue(value) {
1803
- return normalizeDateValueForWrite(value)
1803
+ return normalizeDateValueForWrite(value, {timeZone: this.getModelClass()._timeZoneForDateWrite()})
1804
1804
  }
1805
1805
 
1806
1806
  /**
@@ -2164,7 +2164,7 @@ class VelociousDatabaseRecord {
2164
2164
  * @returns {?} - Normalized value.
2165
2165
  */
2166
2166
  static _normalizeDateValueForInsert(value) {
2167
- return normalizeDateValueForWrite(value)
2167
+ return normalizeDateValueForWrite(value, {timeZone: this._timeZoneForDateWrite()})
2168
2168
  }
2169
2169
 
2170
2170
  /**
@@ -2173,7 +2173,17 @@ class VelociousDatabaseRecord {
2173
2173
  * @returns {string | Date} - Parsed date or original string.
2174
2174
  */
2175
2175
  static _normalizeDateStringForInsert(value) {
2176
- return normalizeDateStringForWrite(value)
2176
+ return normalizeDateStringForWrite(value, {timeZone: this._timeZoneForDateWrite()})
2177
+ }
2178
+
2179
+ /**
2180
+ * Runs time zone for date writes.
2181
+ * @returns {string | undefined} - Active timezone identifier.
2182
+ */
2183
+ static _timeZoneForDateWrite() {
2184
+ const configuration = this._getConfiguration()
2185
+
2186
+ return configuration.getEnvironmentHandler().getTimeZone(configuration)
2177
2187
  }
2178
2188
 
2179
2189
  /**
@@ -3923,7 +3933,7 @@ class VelociousDatabaseRecord {
3923
3933
 
3924
3934
  const value = data[columnName]
3925
3935
 
3926
- data[columnName] = normalizeDateValueForWrite(value)
3936
+ data[columnName] = normalizeDateValueForWrite(value, {timeZone: this.getModelClass()._timeZoneForDateWrite()})
3927
3937
  }
3928
3938
  }
3929
3939
 
@@ -1,5 +1,7 @@
1
1
  // @ts-check
2
2
 
3
+ import {validateTimeZone} from "../time-zone.js"
4
+
3
5
  /**
4
6
  * CommandFileObjectType type.
5
7
  * @typedef {object} CommandFileObjectType
@@ -69,6 +71,26 @@ export default class VelociousEnvironmentHandlerBase {
69
71
  }
70
72
  }
71
73
 
74
+ /**
75
+ * Runs run with timezone.
76
+ * @param {string} timeZone - IANA timezone identifier.
77
+ * @param {() => Promise<?>} callback - Callback to run.
78
+ * @returns {Promise<?>} - Result of the callback.
79
+ */
80
+ async runWithTimezone(timeZone, callback) {
81
+ if (!this.configuration) throw new Error("Configuration hasn't been set")
82
+
83
+ const previousTimeZone = this.configuration._timeZone
84
+
85
+ this.configuration._timeZone = validateTimeZone(timeZone, "timeZone")
86
+
87
+ try {
88
+ return await callback()
89
+ } finally {
90
+ this.configuration._timeZone = previousTimeZone
91
+ }
92
+ }
93
+
72
94
  /**
73
95
  * Runs set timezone offset.
74
96
  * @param {number} _offsetMinutes - Offset in minutes (Date#getTimezoneOffset).
@@ -83,6 +105,17 @@ export default class VelociousEnvironmentHandlerBase {
83
105
  this.configuration._timezoneOffsetMinutes = _offsetMinutes
84
106
  }
85
107
 
108
+ /**
109
+ * Runs set timezone.
110
+ * @param {string} timeZone - IANA timezone identifier.
111
+ * @returns {void} - No return value.
112
+ */
113
+ setTimezone(timeZone) {
114
+ if (!this.configuration) throw new Error("Configuration hasn't been set")
115
+
116
+ this.configuration._timeZone = validateTimeZone(timeZone, "timeZone")
117
+ }
118
+
86
119
  /**
87
120
  * Runs get timezone offset minutes.
88
121
  * @param {import("../configuration.js").default | undefined} configuration - Configuration instance.
@@ -100,6 +133,19 @@ export default class VelociousEnvironmentHandlerBase {
100
133
  return /** @type {number} */ (activeConfiguration.getTimezoneOffsetMinutes())
101
134
  }
102
135
 
136
+ /**
137
+ * Runs get timezone.
138
+ * @param {import("../configuration.js").default | undefined} configuration - Configuration instance.
139
+ * @returns {string | undefined} - Timezone identifier.
140
+ */
141
+ getTimeZone(configuration) {
142
+ const activeConfiguration = configuration || this.configuration
143
+
144
+ if (!activeConfiguration) throw new Error("Configuration hasn't been set")
145
+
146
+ return activeConfiguration.getTimeZone()
147
+ }
148
+
103
149
  /**
104
150
  * Runs run with request timing.
105
151
  * @param {import("../http-server/client/request-timing.js").default | undefined} requestTiming - Request timing collector.