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.
- package/README.md +4 -2
- package/build/configuration-types.js +1 -0
- package/build/configuration.js +27 -1
- package/build/controller.js +13 -1
- package/build/database/datetime-storage.js +49 -5
- package/build/database/record/index.js +14 -4
- package/build/environment-handlers/base.js +46 -0
- package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +362 -38
- package/build/environment-handlers/node.js +84 -8
- package/build/frontend-model-controller.js +7 -7
- package/build/frontend-models/base.js +75 -9
- package/build/frontend-models/transport-serialization.js +41 -8
- package/build/frontend-models/websocket-channel.js +18 -1
- package/build/frontend-models/websocket-publishers.js +12 -1
- package/build/http-server/client/request-runner.js +13 -1
- package/build/src/configuration-types.d.ts +5 -0
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +2 -1
- package/build/src/configuration.d.ts +14 -1
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +25 -2
- package/build/src/controller.d.ts +5 -0
- package/build/src/controller.d.ts.map +1 -1
- package/build/src/controller.js +12 -2
- package/build/src/database/datetime-storage.d.ts +12 -4
- package/build/src/database/datetime-storage.d.ts.map +1 -1
- package/build/src/database/datetime-storage.js +48 -6
- package/build/src/database/record/index.d.ts +5 -0
- package/build/src/database/record/index.d.ts.map +1 -1
- package/build/src/database/record/index.js +13 -5
- package/build/src/environment-handlers/base.d.ts +19 -0
- package/build/src/environment-handlers/base.d.ts.map +1 -1
- package/build/src/environment-handlers/base.js +41 -1
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts +219 -40
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +312 -39
- package/build/src/environment-handlers/node.d.ts +1 -0
- package/build/src/environment-handlers/node.d.ts.map +1 -1
- package/build/src/environment-handlers/node.js +76 -9
- package/build/src/frontend-model-controller.js +8 -8
- package/build/src/frontend-models/base.d.ts +4 -0
- package/build/src/frontend-models/base.d.ts.map +1 -1
- package/build/src/frontend-models/base.js +64 -9
- package/build/src/frontend-models/transport-serialization.d.ts +30 -1
- package/build/src/frontend-models/transport-serialization.d.ts.map +1 -1
- package/build/src/frontend-models/transport-serialization.js +34 -9
- package/build/src/frontend-models/websocket-channel.d.ts.map +1 -1
- package/build/src/frontend-models/websocket-channel.js +16 -2
- package/build/src/frontend-models/websocket-publishers.d.ts.map +1 -1
- package/build/src/frontend-models/websocket-publishers.js +12 -2
- package/build/src/http-server/client/request-runner.d.ts.map +1 -1
- package/build/src/http-server/client/request-runner.js +13 -2
- package/build/src/time-zone.d.ts +16 -0
- package/build/src/time-zone.d.ts.map +1 -0
- package/build/src/time-zone.js +56 -0
- package/build/time-zone.js +65 -0
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -1
- package/scripts/test-browser.js +1 -0
- package/src/configuration-types.js +1 -0
- package/src/configuration.js +27 -1
- package/src/controller.js +13 -1
- package/src/database/datetime-storage.js +49 -5
- package/src/database/record/index.js +14 -4
- package/src/environment-handlers/base.js +46 -0
- package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +362 -38
- package/src/environment-handlers/node.js +84 -8
- package/src/frontend-model-controller.js +7 -7
- package/src/frontend-models/base.js +75 -9
- package/src/frontend-models/transport-serialization.js +41 -8
- package/src/frontend-models/websocket-channel.js +18 -1
- package/src/frontend-models/websocket-publishers.js +12 -1
- package/src/http-server/client/request-runner.js +13 -1
- package/src/time-zone.js +65 -0
|
@@ -32,10 +32,11 @@ import path from "path"
|
|
|
32
32
|
import {AsyncLocalStorage as NodeAsyncLocalStorage} from "node:async_hooks"
|
|
33
33
|
import {timingSafeEqual} from "node:crypto"
|
|
34
34
|
import toImportSpecifier from "../utils/to-import-specifier.js"
|
|
35
|
+
import {validateTimeZone} from "../time-zone.js"
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
38
|
* Defines this typedef.
|
|
38
|
-
* @typedef {{ability?: import("../authorization/ability.js").default, offsetMinutes: number, requestTiming?: import("../http-server/client/request-timing.js").default, tenant?:
|
|
39
|
+
* @typedef {{ability?: import("../authorization/ability.js").default, offsetMinutes: number, requestTiming?: import("../http-server/client/request-timing.js").default, tenant?: ?, timeZone?: string}} TimezoneStore */
|
|
39
40
|
|
|
40
41
|
/**
|
|
41
42
|
* Runs path within allowed prefixes.
|
|
@@ -217,7 +218,30 @@ export default class VelociousEnvironmentHandlerNode extends Base{
|
|
|
217
218
|
ability: existingStore?.ability,
|
|
218
219
|
offsetMinutes,
|
|
219
220
|
requestTiming: existingStore?.requestTiming,
|
|
220
|
-
tenant: existingStore?.tenant
|
|
221
|
+
tenant: existingStore?.tenant,
|
|
222
|
+
timeZone: existingStore?.timeZone ?? this.getTimeZone(this.getConfiguration())
|
|
223
|
+
}, callback)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Runs run with timezone.
|
|
228
|
+
* @param {string} timeZone - IANA timezone identifier.
|
|
229
|
+
* @param {() => Promise<?>} callback - Callback to run.
|
|
230
|
+
* @returns {Promise<?>} - Result of the callback.
|
|
231
|
+
*/
|
|
232
|
+
async runWithTimezone(timeZone, callback) {
|
|
233
|
+
if (!this._timezoneAsyncLocalStorage) {
|
|
234
|
+
return await super.runWithTimezone(timeZone, callback)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const existingStore = this._timezoneAsyncLocalStorage.getStore()
|
|
238
|
+
|
|
239
|
+
return await this._timezoneAsyncLocalStorage.run({
|
|
240
|
+
ability: existingStore?.ability,
|
|
241
|
+
offsetMinutes: existingStore?.offsetMinutes ?? this.getTimezoneOffsetMinutes(this.getConfiguration()),
|
|
242
|
+
requestTiming: existingStore?.requestTiming,
|
|
243
|
+
tenant: existingStore?.tenant,
|
|
244
|
+
timeZone: validateTimeZone(timeZone, "timeZone")
|
|
221
245
|
}, callback)
|
|
222
246
|
}
|
|
223
247
|
|
|
@@ -240,7 +264,37 @@ export default class VelociousEnvironmentHandlerNode extends Base{
|
|
|
240
264
|
ability: existingStore?.ability,
|
|
241
265
|
offsetMinutes,
|
|
242
266
|
requestTiming: existingStore?.requestTiming,
|
|
243
|
-
tenant: existingStore?.tenant
|
|
267
|
+
tenant: existingStore?.tenant,
|
|
268
|
+
timeZone: existingStore?.timeZone ?? this.getTimeZone(this.getConfiguration())
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Runs set timezone.
|
|
275
|
+
* @param {string} timeZone - IANA timezone identifier.
|
|
276
|
+
* @returns {void} - No return value.
|
|
277
|
+
*/
|
|
278
|
+
setTimezone(timeZone) {
|
|
279
|
+
if (!this._timezoneAsyncLocalStorage) {
|
|
280
|
+
super.setTimezone(timeZone)
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const normalizedTimeZone = validateTimeZone(timeZone, "timeZone")
|
|
285
|
+
const store = this._timezoneAsyncLocalStorage.getStore()
|
|
286
|
+
|
|
287
|
+
if (store) {
|
|
288
|
+
store.timeZone = normalizedTimeZone
|
|
289
|
+
} else {
|
|
290
|
+
const existingStore = this._timezoneAsyncLocalStorage.getStore()
|
|
291
|
+
|
|
292
|
+
this._timezoneAsyncLocalStorage.enterWith({
|
|
293
|
+
ability: existingStore?.ability,
|
|
294
|
+
offsetMinutes: existingStore?.offsetMinutes ?? this.getTimezoneOffsetMinutes(this.getConfiguration()),
|
|
295
|
+
requestTiming: existingStore?.requestTiming,
|
|
296
|
+
tenant: existingStore?.tenant,
|
|
297
|
+
timeZone: normalizedTimeZone
|
|
244
298
|
})
|
|
245
299
|
}
|
|
246
300
|
}
|
|
@@ -262,6 +316,23 @@ export default class VelociousEnvironmentHandlerNode extends Base{
|
|
|
262
316
|
return super.getTimezoneOffsetMinutes(configuration)
|
|
263
317
|
}
|
|
264
318
|
|
|
319
|
+
/**
|
|
320
|
+
* Runs get timezone.
|
|
321
|
+
* @param {import("../configuration.js").default | undefined} configuration - Configuration instance.
|
|
322
|
+
* @returns {string | undefined} - Timezone identifier.
|
|
323
|
+
*/
|
|
324
|
+
getTimeZone(configuration) {
|
|
325
|
+
if (this._timezoneAsyncLocalStorage) {
|
|
326
|
+
const store = this._timezoneAsyncLocalStorage.getStore()
|
|
327
|
+
|
|
328
|
+
if (store && typeof store.timeZone === "string") {
|
|
329
|
+
return store.timeZone
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return super.getTimeZone(configuration)
|
|
334
|
+
}
|
|
335
|
+
|
|
265
336
|
/**
|
|
266
337
|
* Runs run with ability.
|
|
267
338
|
* @param {import("../authorization/ability.js").default | undefined} ability - Ability to set for callback scope.
|
|
@@ -279,7 +350,8 @@ export default class VelociousEnvironmentHandlerNode extends Base{
|
|
|
279
350
|
ability,
|
|
280
351
|
offsetMinutes: existingStore?.offsetMinutes ?? this.getTimezoneOffsetMinutes(this.getConfiguration()),
|
|
281
352
|
requestTiming: existingStore?.requestTiming,
|
|
282
|
-
tenant: existingStore?.tenant
|
|
353
|
+
tenant: existingStore?.tenant,
|
|
354
|
+
timeZone: existingStore?.timeZone ?? this.getTimeZone(this.getConfiguration())
|
|
283
355
|
}, callback)
|
|
284
356
|
}
|
|
285
357
|
|
|
@@ -303,7 +375,8 @@ export default class VelociousEnvironmentHandlerNode extends Base{
|
|
|
303
375
|
ability,
|
|
304
376
|
offsetMinutes: this.getTimezoneOffsetMinutes(this.getConfiguration()),
|
|
305
377
|
requestTiming: undefined,
|
|
306
|
-
tenant: undefined
|
|
378
|
+
tenant: undefined,
|
|
379
|
+
timeZone: this.getTimeZone(this.getConfiguration())
|
|
307
380
|
})
|
|
308
381
|
}
|
|
309
382
|
}
|
|
@@ -337,7 +410,8 @@ export default class VelociousEnvironmentHandlerNode extends Base{
|
|
|
337
410
|
ability: existingStore?.ability,
|
|
338
411
|
offsetMinutes: existingStore?.offsetMinutes ?? this.getTimezoneOffsetMinutes(this.getConfiguration()),
|
|
339
412
|
requestTiming,
|
|
340
|
-
tenant: existingStore?.tenant
|
|
413
|
+
tenant: existingStore?.tenant,
|
|
414
|
+
timeZone: existingStore?.timeZone ?? this.getTimeZone(this.getConfiguration())
|
|
341
415
|
}, callback)
|
|
342
416
|
}
|
|
343
417
|
|
|
@@ -370,7 +444,8 @@ export default class VelociousEnvironmentHandlerNode extends Base{
|
|
|
370
444
|
ability: existingStore?.ability,
|
|
371
445
|
offsetMinutes: existingStore?.offsetMinutes ?? this.getTimezoneOffsetMinutes(this.getConfiguration()),
|
|
372
446
|
requestTiming: existingStore?.requestTiming,
|
|
373
|
-
tenant
|
|
447
|
+
tenant,
|
|
448
|
+
timeZone: existingStore?.timeZone ?? this.getTimeZone(this.getConfiguration())
|
|
374
449
|
}, callback)
|
|
375
450
|
}
|
|
376
451
|
|
|
@@ -394,7 +469,8 @@ export default class VelociousEnvironmentHandlerNode extends Base{
|
|
|
394
469
|
ability: undefined,
|
|
395
470
|
offsetMinutes: this.getTimezoneOffsetMinutes(this.getConfiguration()),
|
|
396
471
|
requestTiming: undefined,
|
|
397
|
-
tenant
|
|
472
|
+
tenant,
|
|
473
|
+
timeZone: this.getTimeZone(this.getConfiguration())
|
|
398
474
|
})
|
|
399
475
|
}
|
|
400
476
|
}
|
|
@@ -2315,7 +2315,7 @@ export default class FrontendModelController extends Controller {
|
|
|
2315
2315
|
const isDateTimeColumn = typeof columnType === "string" && ["date", "datetime", "timestamp"].some((type) => columnType.includes(type))
|
|
2316
2316
|
|
|
2317
2317
|
if (isDateTimeColumn) {
|
|
2318
|
-
const parsedDate = normalizeDateStringForWrite(value)
|
|
2318
|
+
const parsedDate = normalizeDateStringForWrite(value, {timeZone: this.getConfiguration().getEnvironmentHandler().getTimeZone(this.getConfiguration())})
|
|
2319
2319
|
|
|
2320
2320
|
if (isDate(parsedDate)) {
|
|
2321
2321
|
return parsedDate
|
|
@@ -3042,7 +3042,7 @@ export default class FrontendModelController extends Controller {
|
|
|
3042
3042
|
json: /** @type {Record<string, ?>} */ (serializeFrontendModelTransportValue({
|
|
3043
3043
|
errorMessage: frontendModelClientSafeErrorMessage,
|
|
3044
3044
|
status: "error"
|
|
3045
|
-
}))
|
|
3045
|
+
}, this.transportSerializationOptions()))
|
|
3046
3046
|
})
|
|
3047
3047
|
}
|
|
3048
3048
|
|
|
@@ -3203,7 +3203,7 @@ export default class FrontendModelController extends Controller {
|
|
|
3203
3203
|
if (!responsePayload) return
|
|
3204
3204
|
|
|
3205
3205
|
await this.render({
|
|
3206
|
-
json: /** @type {Record<string, ?>} */ (serializeFrontendModelTransportValue(responsePayload))
|
|
3206
|
+
json: /** @type {Record<string, ?>} */ (serializeFrontendModelTransportValue(responsePayload, this.transportSerializationOptions()))
|
|
3207
3207
|
})
|
|
3208
3208
|
} catch (error) {
|
|
3209
3209
|
const errorContext = this.frontendModelEndpointErrorContext({action, commandType: action, error})
|
|
@@ -3211,7 +3211,7 @@ export default class FrontendModelController extends Controller {
|
|
|
3211
3211
|
await this.frontendModelLogEndpointError({action, commandType: action, error, model: errorContext.model})
|
|
3212
3212
|
|
|
3213
3213
|
await this.render({
|
|
3214
|
-
json: /** @type {Record<string, ?>} */ (serializeFrontendModelTransportValue(await this.frontendModelClientErrorPayloadForError(error, errorContext)))
|
|
3214
|
+
json: /** @type {Record<string, ?>} */ (serializeFrontendModelTransportValue(await this.frontendModelClientErrorPayloadForError(error, errorContext), this.transportSerializationOptions()))
|
|
3215
3215
|
})
|
|
3216
3216
|
}
|
|
3217
3217
|
}
|
|
@@ -3529,7 +3529,7 @@ export default class FrontendModelController extends Controller {
|
|
|
3529
3529
|
json: /** @type {Record<string, ?>} */ (serializeFrontendModelTransportValue({
|
|
3530
3530
|
responses,
|
|
3531
3531
|
status: "success"
|
|
3532
|
-
}))
|
|
3532
|
+
}, this.transportSerializationOptions()))
|
|
3533
3533
|
})
|
|
3534
3534
|
}
|
|
3535
3535
|
|
|
@@ -3736,7 +3736,7 @@ export default class FrontendModelController extends Controller {
|
|
|
3736
3736
|
const responsePayload = await this.frontendModelCustomCommandPayload()
|
|
3737
3737
|
|
|
3738
3738
|
await this.render({
|
|
3739
|
-
json: /** @type {Record<string, ?>} */ (serializeFrontendModelTransportValue(responsePayload))
|
|
3739
|
+
json: /** @type {Record<string, ?>} */ (serializeFrontendModelTransportValue(responsePayload, this.transportSerializationOptions()))
|
|
3740
3740
|
})
|
|
3741
3741
|
} catch (error) {
|
|
3742
3742
|
const errorContext = this.frontendModelEndpointErrorContext({action: "frontendCustomCommand", commandType: "custom-command", error})
|
|
@@ -3744,7 +3744,7 @@ export default class FrontendModelController extends Controller {
|
|
|
3744
3744
|
await this.frontendModelLogEndpointError({action: errorContext.action, commandType: errorContext.commandType, error, model: errorContext.model})
|
|
3745
3745
|
|
|
3746
3746
|
await this.render({
|
|
3747
|
-
json: /** @type {Record<string, ?>} */ (serializeFrontendModelTransportValue(await this.frontendModelClientErrorPayloadForError(error, errorContext)))
|
|
3747
|
+
json: /** @type {Record<string, ?>} */ (serializeFrontendModelTransportValue(await this.frontendModelClientErrorPayloadForError(error, errorContext), this.transportSerializationOptions()))
|
|
3748
3748
|
})
|
|
3749
3749
|
}
|
|
3750
3750
|
}
|
|
@@ -5,10 +5,11 @@ 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 {
|
|
8
|
+
import {normalizeDateStringForWrite} from "../database/datetime-storage.js"
|
|
9
9
|
import {registerFrontendModel, resolveFrontendModelClass} from "./model-registry.js"
|
|
10
10
|
import {validateFrontendModelResourceCommandName, validateFrontendModelResourcePath} from "./resource-config-validation.js"
|
|
11
11
|
import {deserializeFrontendModelTransportValue, serializeFrontendModelTransportValue} from "./transport-serialization.js"
|
|
12
|
+
import {REQUEST_TIME_ZONE_HEADER, validateTimeZone} from "../time-zone.js"
|
|
12
13
|
import VelociousWebsocketClient from "../http-client/websocket-client.js"
|
|
13
14
|
import {bufferOutgoingEvent, drainBufferedOutgoingEvents} from "./outgoing-event-buffer.js"
|
|
14
15
|
import {defineModelScope} from "../utils/model-scope.js"
|
|
@@ -102,6 +103,7 @@ import {readPayloadAssociationCount, readPayloadComputedAbility, readPayloadQuer
|
|
|
102
103
|
* @property {{post: (path: string, body?: ?, options?: {headers?: Record<string, string>}) => Promise<{json: () => ?}>, subscribe: (channel: string, options: {params?: Record<string, ?>}, callback: (payload: ?) => void) => (() => void), subscribeAndWait?: (channel: string, options: {params?: Record<string, ?>}, callback: (payload: ?) => void) => Promise<(() => void)>}} [websocketClient] - Optional websocket client for shared frontend-model API requests and subscriptions.
|
|
103
104
|
* @property {Record<string, string> | (() => Record<string, string>)} [requestHeaders] - Extra HTTP/WS headers to attach to every frontend-model API request. Pass a function to compute them at request time (for example to include the current locale).
|
|
104
105
|
* @property {{get: () => string | null | undefined | Promise<string | null | undefined>, set: (sessionId: string) => void | Promise<void>, clear: () => void | Promise<void>}} [sessionStore] - Optional sessionId persistence hook forwarded to the internal `VelociousWebsocketClient` so WS sessions can be resumed across page reloads / app restarts.
|
|
106
|
+
* @property {string | (() => string | null | undefined)} [timeZone] - IANA timezone sent with every frontend-model API request for timezone-less datetime parsing.
|
|
105
107
|
*/
|
|
106
108
|
/**
|
|
107
109
|
* FrontendModelIdleWaitArgs type.
|
|
@@ -1590,16 +1592,69 @@ function frontendModelTransportPath(url) {
|
|
|
1590
1592
|
}
|
|
1591
1593
|
}
|
|
1592
1594
|
|
|
1595
|
+
/**
|
|
1596
|
+
* Resolves the browser runtime timezone when available.
|
|
1597
|
+
* @returns {string | undefined} - Browser runtime timezone when available.
|
|
1598
|
+
*/
|
|
1599
|
+
function defaultFrontendModelTimeZone() {
|
|
1600
|
+
if (typeof window === "undefined") return undefined
|
|
1601
|
+
|
|
1602
|
+
const intl = globalThis.Intl
|
|
1603
|
+
|
|
1604
|
+
if (!intl) {
|
|
1605
|
+
throw new Error("Expected Intl to be available for browser timezone detection")
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
if (typeof intl.DateTimeFormat !== "function") {
|
|
1609
|
+
throw new Error("Expected Intl.DateTimeFormat to be available as a function")
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
const timeZone = intl.DateTimeFormat().resolvedOptions().timeZone
|
|
1613
|
+
|
|
1614
|
+
if (typeof timeZone !== "string" || timeZone.trim().length < 1) {
|
|
1615
|
+
throw new Error("Expected Intl.DateTimeFormat to resolve a browser timezone string")
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
return validateTimeZone(timeZone, "browser timeZone")
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
/**
|
|
1622
|
+
* Resolves the configured frontend-model request timezone.
|
|
1623
|
+
* @returns {string | undefined} - Configured frontend-model timezone.
|
|
1624
|
+
*/
|
|
1625
|
+
function frontendModelTransportTimeZone() {
|
|
1626
|
+
if (!Object.prototype.hasOwnProperty.call(frontendModelTransportConfig, "timeZone")) {
|
|
1627
|
+
return defaultFrontendModelTimeZone()
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
const timeZone = typeof frontendModelTransportConfig.timeZone === "function"
|
|
1631
|
+
? frontendModelTransportConfig.timeZone()
|
|
1632
|
+
: frontendModelTransportConfig.timeZone
|
|
1633
|
+
|
|
1634
|
+
if (timeZone === undefined || timeZone === null) {
|
|
1635
|
+
throw new Error("Frontend model transport timeZone did not resolve to a timezone string")
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
return validateTimeZone(timeZone, "frontend model transport timeZone")
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1593
1641
|
/**
|
|
1594
1642
|
* Runs frontend model request headers.
|
|
1643
|
+
* @param {string | undefined} [timeZone] - Pre-resolved timezone for this request.
|
|
1595
1644
|
* @returns {Record<string, string>} - Headers for frontend-model HTTP requests.
|
|
1596
1645
|
*/
|
|
1597
|
-
function frontendModelRequestHeaders() {
|
|
1646
|
+
function frontendModelRequestHeaders(timeZone = frontendModelTransportTimeZone()) {
|
|
1598
1647
|
const dynamicHeaders = typeof frontendModelTransportConfig.requestHeaders === "function"
|
|
1599
1648
|
? (frontendModelTransportConfig.requestHeaders() || {})
|
|
1600
1649
|
: (frontendModelTransportConfig.requestHeaders || {})
|
|
1650
|
+
/** @type {Record<string, string>} */
|
|
1651
|
+
const headers = {"Content-Type": "application/json", ...dynamicHeaders}
|
|
1652
|
+
|
|
1653
|
+
if (timeZone) {
|
|
1654
|
+
headers[REQUEST_TIME_ZONE_HEADER] = timeZone
|
|
1655
|
+
}
|
|
1601
1656
|
|
|
1602
|
-
return
|
|
1657
|
+
return headers
|
|
1603
1658
|
}
|
|
1604
1659
|
|
|
1605
1660
|
/**
|
|
@@ -1608,10 +1663,11 @@ function frontendModelRequestHeaders() {
|
|
|
1608
1663
|
* @returns {Promise<Record<string, ?>>} - Decoded shared frontend-model API response.
|
|
1609
1664
|
*/
|
|
1610
1665
|
async function performSharedFrontendModelApiRequest(requestPayload) {
|
|
1611
|
-
const
|
|
1666
|
+
const timeZone = frontendModelTransportTimeZone()
|
|
1667
|
+
const serializedRequestPayload = serializeFrontendModelTransportValue(requestPayload, {timeZone})
|
|
1612
1668
|
const websocketClient = frontendModelTransportConfig.websocketClient
|
|
1613
1669
|
const url = frontendModelApiUrl()
|
|
1614
|
-
const mergedHeaders = frontendModelRequestHeaders()
|
|
1670
|
+
const mergedHeaders = frontendModelRequestHeaders(timeZone)
|
|
1615
1671
|
|
|
1616
1672
|
if (websocketClient) {
|
|
1617
1673
|
const response = await websocketClient.post(frontendModelTransportPath(url), serializedRequestPayload, {
|
|
@@ -2710,6 +2766,14 @@ export default class FrontendModelBase {
|
|
|
2710
2766
|
frontendModelTransportConfig.requestHeaders = config.requestHeaders
|
|
2711
2767
|
}
|
|
2712
2768
|
|
|
2769
|
+
if (Object.prototype.hasOwnProperty.call(config, "timeZone")) {
|
|
2770
|
+
if (config.timeZone === undefined) {
|
|
2771
|
+
delete frontendModelTransportConfig.timeZone
|
|
2772
|
+
} else {
|
|
2773
|
+
frontendModelTransportConfig.timeZone = config.timeZone
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2713
2777
|
if (Object.prototype.hasOwnProperty.call(config, "sessionStore")) {
|
|
2714
2778
|
frontendModelTransportConfig.sessionStore = config.sessionStore
|
|
2715
2779
|
// Reset cached internal client so the new sessionStore is picked up.
|
|
@@ -3706,7 +3770,7 @@ export default class FrontendModelBase {
|
|
|
3706
3770
|
*/
|
|
3707
3771
|
static findByPrimitiveValuesMatch(actualValue, expectedValue) {
|
|
3708
3772
|
if (actualValue instanceof Date && typeof expectedValue === "string") {
|
|
3709
|
-
const normalizedExpectedValue = normalizeDateStringForWrite(expectedValue)
|
|
3773
|
+
const normalizedExpectedValue = normalizeDateStringForWrite(expectedValue, {timeZone: frontendModelTransportTimeZone()})
|
|
3710
3774
|
|
|
3711
3775
|
if (normalizedExpectedValue instanceof Date) {
|
|
3712
3776
|
return actualValue.toISOString() === normalizedExpectedValue.toISOString()
|
|
@@ -4183,7 +4247,8 @@ export default class FrontendModelBase {
|
|
|
4183
4247
|
*/
|
|
4184
4248
|
static async executeCommand(commandType, payload) {
|
|
4185
4249
|
const commandName = this.commandName(commandType)
|
|
4186
|
-
const
|
|
4250
|
+
const timeZone = frontendModelTransportTimeZone()
|
|
4251
|
+
const serializedPayload = /** @type {Record<string, ?>} */ (serializeFrontendModelTransportValue(payload, {timeZone}))
|
|
4187
4252
|
const resourcePath = this.resourcePath()
|
|
4188
4253
|
const containsAttachmentUpload = frontendModelPayloadContainsAttachmentUpload(serializedPayload)
|
|
4189
4254
|
const useSharedTransport = !containsAttachmentUpload
|
|
@@ -4219,7 +4284,7 @@ export default class FrontendModelBase {
|
|
|
4219
4284
|
const directResponse = await fetch(url, {
|
|
4220
4285
|
body: JSON.stringify(serializedPayload),
|
|
4221
4286
|
credentials: "include",
|
|
4222
|
-
headers: frontendModelRequestHeaders(),
|
|
4287
|
+
headers: frontendModelRequestHeaders(timeZone),
|
|
4223
4288
|
method: "POST"
|
|
4224
4289
|
})
|
|
4225
4290
|
|
|
@@ -4257,7 +4322,8 @@ export default class FrontendModelBase {
|
|
|
4257
4322
|
* @returns {Promise<Record<string, FrontendModelAttributeValue>>} - Decoded response payload.
|
|
4258
4323
|
*/
|
|
4259
4324
|
static async executeCustomCommand({commandName, commandType, memberId = null, payload, resourcePath}) {
|
|
4260
|
-
const
|
|
4325
|
+
const timeZone = frontendModelTransportTimeZone()
|
|
4326
|
+
const serializedPayload = /** @type {Record<string, ?>} */ (serializeFrontendModelTransportValue(payload, {timeZone}))
|
|
4261
4327
|
const customPath = frontendModelCustomCommandPath({
|
|
4262
4328
|
commandName,
|
|
4263
4329
|
memberId,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import {resolveFrontendModelClass} from "./model-registry.js"
|
|
4
4
|
import isPlainObject from "../utils/plain-object.js"
|
|
5
|
+
import {formatDateInTimeZone, validateTimeZone} from "../time-zone.js"
|
|
5
6
|
|
|
6
7
|
const TYPE_KEY = "__velocious_type"
|
|
7
8
|
const TYPE_DATE = "date"
|
|
@@ -14,6 +15,18 @@ const NUMBER_POSITIVE_INFINITY = "Infinity"
|
|
|
14
15
|
const NUMBER_NEGATIVE_INFINITY = "-Infinity"
|
|
15
16
|
const PRELOADED_RELATIONSHIPS_KEY = "__preloadedRelationships"
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Frontend model transport serialization options.
|
|
20
|
+
* @typedef {object} FrontendModelTransportSerializationOptions
|
|
21
|
+
* @property {string | undefined} [timeZone] - IANA timezone used when serializing Date instants.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Normalized frontend model transport serialization options.
|
|
26
|
+
* @typedef {object} NormalizedFrontendModelTransportSerializationOptions
|
|
27
|
+
* @property {string | undefined} timeZone - Validated IANA timezone used when serializing Date instants.
|
|
28
|
+
*/
|
|
29
|
+
|
|
17
30
|
/**
|
|
18
31
|
* Assign a key to a plain object without triggering the `__proto__` setter.
|
|
19
32
|
* Uses `Object.defineProperty` so that keys like `__proto__` are stored as
|
|
@@ -150,9 +163,10 @@ export function isBackendModelInstance(value) {
|
|
|
150
163
|
* Runs serialize frontend model transport value internal.
|
|
151
164
|
* @param {?} value - Value to serialize.
|
|
152
165
|
* @param {WeakSet<object>} seenModels - Models already visited in the current recursion path.
|
|
166
|
+
* @param {NormalizedFrontendModelTransportSerializationOptions} options - Serialization options.
|
|
153
167
|
* @returns {?} - Serialized value with transport markers.
|
|
154
168
|
*/
|
|
155
|
-
function serializeFrontendModelTransportValueInternal(value, seenModels) {
|
|
169
|
+
function serializeFrontendModelTransportValueInternal(value, seenModels, options) {
|
|
156
170
|
if (value === undefined) {
|
|
157
171
|
return {[TYPE_KEY]: TYPE_UNDEFINED}
|
|
158
172
|
}
|
|
@@ -160,7 +174,7 @@ function serializeFrontendModelTransportValueInternal(value, seenModels) {
|
|
|
160
174
|
if (value instanceof Date) {
|
|
161
175
|
return {
|
|
162
176
|
[TYPE_KEY]: TYPE_DATE,
|
|
163
|
-
value: value.toISOString()
|
|
177
|
+
value: options.timeZone ? formatDateInTimeZone(value, options.timeZone) : value.toISOString()
|
|
164
178
|
}
|
|
165
179
|
}
|
|
166
180
|
|
|
@@ -183,7 +197,7 @@ function serializeFrontendModelTransportValueInternal(value, seenModels) {
|
|
|
183
197
|
}
|
|
184
198
|
|
|
185
199
|
if (Array.isArray(value)) {
|
|
186
|
-
return value.map((entry) => serializeFrontendModelTransportValueInternal(entry, seenModels))
|
|
200
|
+
return value.map((entry) => serializeFrontendModelTransportValueInternal(entry, seenModels, options))
|
|
187
201
|
}
|
|
188
202
|
|
|
189
203
|
if (isBackendModelInstance(value)) {
|
|
@@ -195,7 +209,7 @@ function serializeFrontendModelTransportValueInternal(value, seenModels) {
|
|
|
195
209
|
* @type {Record<string, ?>} */
|
|
196
210
|
const serializedModel = {
|
|
197
211
|
[TYPE_KEY]: TYPE_FRONTEND_MODEL,
|
|
198
|
-
attributes: /** @type {Record<string, ?>} */ (serializeFrontendModelTransportValueInternal(modelAttributes, seenModels)),
|
|
212
|
+
attributes: /** @type {Record<string, ?>} */ (serializeFrontendModelTransportValueInternal(modelAttributes, seenModels, options)),
|
|
199
213
|
modelName
|
|
200
214
|
}
|
|
201
215
|
|
|
@@ -220,7 +234,8 @@ function serializeFrontendModelTransportValueInternal(value, seenModels) {
|
|
|
220
234
|
|
|
221
235
|
assignSafeProperty(preloadedRelationships, relationshipName, serializeFrontendModelTransportValueInternal(
|
|
222
236
|
loadedRelationship == undefined ? null : loadedRelationship,
|
|
223
|
-
seenModels
|
|
237
|
+
seenModels,
|
|
238
|
+
options
|
|
224
239
|
))
|
|
225
240
|
}
|
|
226
241
|
|
|
@@ -240,7 +255,7 @@ function serializeFrontendModelTransportValueInternal(value, seenModels) {
|
|
|
240
255
|
const serialized = {}
|
|
241
256
|
|
|
242
257
|
for (const [key, nestedValue] of Object.entries(value)) {
|
|
243
|
-
assignSafeProperty(serialized, key, serializeFrontendModelTransportValueInternal(nestedValue, seenModels))
|
|
258
|
+
assignSafeProperty(serialized, key, serializeFrontendModelTransportValueInternal(nestedValue, seenModels, options))
|
|
244
259
|
}
|
|
245
260
|
|
|
246
261
|
return serialized
|
|
@@ -249,6 +264,19 @@ function serializeFrontendModelTransportValueInternal(value, seenModels) {
|
|
|
249
264
|
return value
|
|
250
265
|
}
|
|
251
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Normalizes serializer options once per top-level serialization.
|
|
269
|
+
* @param {FrontendModelTransportSerializationOptions} options - Serialization options.
|
|
270
|
+
* @returns {NormalizedFrontendModelTransportSerializationOptions} - Normalized options.
|
|
271
|
+
*/
|
|
272
|
+
function normalizeFrontendModelTransportSerializationOptions(options) {
|
|
273
|
+
return {
|
|
274
|
+
timeZone: options.timeZone === undefined
|
|
275
|
+
? undefined
|
|
276
|
+
: validateTimeZone(options.timeZone, "transport serialization timeZone")
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
252
280
|
/**
|
|
253
281
|
* Runs deserialize frontend model marker.
|
|
254
282
|
* @param {{attributes: Record<string, ?>, modelName: string, preloadedRelationships?: Record<string, ?>}} marker - Encoded frontend-model marker.
|
|
@@ -292,10 +320,15 @@ function deserializeFrontendModelMarker(marker) {
|
|
|
292
320
|
/**
|
|
293
321
|
* Runs the serializeFrontendModelTransportValue helper.
|
|
294
322
|
* @param {?} value - Value to serialize.
|
|
323
|
+
* @param {FrontendModelTransportSerializationOptions} [options] - Serialization options.
|
|
295
324
|
* @returns {?} - Serialized value with transport markers.
|
|
296
325
|
*/
|
|
297
|
-
export function serializeFrontendModelTransportValue(value) {
|
|
298
|
-
return serializeFrontendModelTransportValueInternal(
|
|
326
|
+
export function serializeFrontendModelTransportValue(value, options = {}) {
|
|
327
|
+
return serializeFrontendModelTransportValueInternal(
|
|
328
|
+
value,
|
|
329
|
+
new WeakSet(),
|
|
330
|
+
normalizeFrontendModelTransportSerializationOptions(options)
|
|
331
|
+
)
|
|
299
332
|
}
|
|
300
333
|
|
|
301
334
|
/**
|
|
@@ -10,6 +10,17 @@ const EVENT_FILTER_KEYS = new Set(["joins", "key", "searches", "where"])
|
|
|
10
10
|
// to avoid the configuration → logger → websocket-publishers import cycle.
|
|
11
11
|
const FRONTEND_MODELS_CHANNEL_NAME = "frontend-models"
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Runs transport serialization options for a configuration.
|
|
15
|
+
* @param {import("../configuration.js").default} configuration - Configuration instance.
|
|
16
|
+
* @returns {import("./transport-serialization.js").FrontendModelTransportSerializationOptions} - Serialization options.
|
|
17
|
+
*/
|
|
18
|
+
function transportSerializationOptionsForConfiguration(configuration) {
|
|
19
|
+
return {
|
|
20
|
+
timeZone: configuration.getEnvironmentHandler().getTimeZone(configuration)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
13
24
|
/**
|
|
14
25
|
* Defines this typedef.
|
|
15
26
|
* @typedef {{action?: string, id?: string | number, matchedEventFilterKeys?: string[], record?: import("./query.js").FrontendModelTransportValue, [key: string]: import("./query.js").FrontendModelTransportValue | string[] | undefined}} FrontendModelLifecycleBroadcastBody
|
|
@@ -164,9 +175,15 @@ export default class FrontendModelWebsocketChannel extends VelociousWebsocketCha
|
|
|
164
175
|
return
|
|
165
176
|
}
|
|
166
177
|
|
|
178
|
+
const configuration = this.session.configuration
|
|
179
|
+
|
|
180
|
+
if (!configuration) {
|
|
181
|
+
throw new Error("Frontend model websocket channel has no configuration for transport serialization")
|
|
182
|
+
}
|
|
183
|
+
|
|
167
184
|
deliverBody = {
|
|
168
185
|
...deliverBody,
|
|
169
|
-
record: /** @type {import("./query.js").FrontendModelTransportValue} */ (serializeFrontendModelTransportValue(projectedRecord))
|
|
186
|
+
record: /** @type {import("./query.js").FrontendModelTransportValue} */ (serializeFrontendModelTransportValue(projectedRecord, transportSerializationOptionsForConfiguration(configuration)))
|
|
170
187
|
}
|
|
171
188
|
}
|
|
172
189
|
|
|
@@ -11,6 +11,17 @@ const channelClassRegisteredConfigurations = new WeakSet()
|
|
|
11
11
|
/** Shared channel name for all frontend-model lifecycle subscriptions. */
|
|
12
12
|
export const FRONTEND_MODELS_CHANNEL_NAME = "frontend-models"
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Runs transport serialization options for a configuration.
|
|
16
|
+
* @param {import("../configuration.js").default} configuration - Configuration instance.
|
|
17
|
+
* @returns {import("./transport-serialization.js").FrontendModelTransportSerializationOptions} - Serialization options.
|
|
18
|
+
*/
|
|
19
|
+
function transportSerializationOptionsForConfiguration(configuration) {
|
|
20
|
+
return {
|
|
21
|
+
timeZone: configuration.getEnvironmentHandler().getTimeZone(configuration)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
14
25
|
/**
|
|
15
26
|
* Runs the frontendModelBroadcastChannelName helper.
|
|
16
27
|
* @param {string} modelName - Model class name.
|
|
@@ -169,7 +180,7 @@ function broadcastFrontendModelEvent(configuration, modelName, event) {
|
|
|
169
180
|
action: event.action,
|
|
170
181
|
id: event.id,
|
|
171
182
|
model: modelName,
|
|
172
|
-
...(event.record ? {record: serializeFrontendModelTransportValue(event.record)} : {})
|
|
183
|
+
...(event.record ? {record: serializeFrontendModelTransportValue(event.record, transportSerializationOptionsForConfiguration(configuration))} : {})
|
|
173
184
|
}
|
|
174
185
|
|
|
175
186
|
configuration.broadcastToChannel(FRONTEND_MODELS_CHANNEL_NAME, {model: modelName}, body)
|
|
@@ -7,6 +7,7 @@ import Logger from "../../logger.js"
|
|
|
7
7
|
import RequestTiming from "./request-timing.js"
|
|
8
8
|
import Response from "./response.js"
|
|
9
9
|
import RoutesResolver from "../../routes/resolver.js"
|
|
10
|
+
import {REQUEST_TIME_ZONE_HEADER} from "../../time-zone.js"
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Runs stack frame line.
|
|
@@ -223,9 +224,10 @@ export default class VelociousHttpServerClientRequestRunner {
|
|
|
223
224
|
|
|
224
225
|
setRequestTimeoutSeconds(configuration.getRequestTimeoutMs?.())
|
|
225
226
|
|
|
227
|
+
/** @type {Promise<void> | undefined} */
|
|
226
228
|
let resolvePromise
|
|
227
229
|
|
|
228
|
-
|
|
230
|
+
const runResolvedRequest = async () => {
|
|
229
231
|
resolvePromise = routesResolver.resolve()
|
|
230
232
|
// Keep Promise.race here to allow dynamic timeout updates.
|
|
231
233
|
await Promise.race([resolvePromise, timeoutPromise])
|
|
@@ -236,6 +238,16 @@ export default class VelociousHttpServerClientRequestRunner {
|
|
|
236
238
|
hasFilePath: Boolean(response.getFilePath()),
|
|
237
239
|
bodyType: responseBodyTypeForLog(response)
|
|
238
240
|
}])
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
const requestTimeZone = request.header(REQUEST_TIME_ZONE_HEADER)
|
|
245
|
+
|
|
246
|
+
if (requestTimeZone !== undefined && requestTimeZone !== null) {
|
|
247
|
+
await configuration.runWithTimezone(requestTimeZone, runResolvedRequest)
|
|
248
|
+
} else {
|
|
249
|
+
await runResolvedRequest()
|
|
250
|
+
}
|
|
239
251
|
} catch (error) {
|
|
240
252
|
if (timedOut && resolvePromise) {
|
|
241
253
|
void resolvePromise.catch((resolveError) => {
|
|
@@ -415,6 +415,7 @@
|
|
|
415
415
|
* @property {TenantDatabaseResolverType} [tenantDatabaseResolver] - Resolver for deriving tenant-specific database config overrides.
|
|
416
416
|
* @property {Record<string, TenantDatabaseProviderType>} [tenantDatabaseProviders] - Tenant database lifecycle providers keyed by database identifier.
|
|
417
417
|
* @property {string} [testing] - Path to the testing configuration file.
|
|
418
|
+
* @property {string | (() => string | undefined)} [timeZone] - Default timezone for timezone-less datetime strings.
|
|
418
419
|
* @property {number | (() => number)} [timezoneOffsetMinutes] - Default timezone offset in minutes.
|
|
419
420
|
* @property {string | string[]} [trustedProxies] - Trusted reverse proxy address ranges used to resolve request remote addresses from forwarding headers.
|
|
420
421
|
* @property {number | (() => number)} [requestTimeoutMs] - Timeout in seconds for completing a HTTP request.
|
|
@@ -1538,6 +1539,10 @@ export type ConfigurationArgsType = {
|
|
|
1538
1539
|
* - Path to the testing configuration file.
|
|
1539
1540
|
*/
|
|
1540
1541
|
testing?: string | undefined;
|
|
1542
|
+
/**
|
|
1543
|
+
* - Default timezone for timezone-less datetime strings.
|
|
1544
|
+
*/
|
|
1545
|
+
timeZone?: string | (() => string | undefined) | undefined;
|
|
1541
1546
|
/**
|
|
1542
1547
|
* - Default timezone offset in minutes.
|
|
1543
1548
|
*/
|