velocious 1.0.570 → 1.0.572
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 +46 -0
- package/build/background-jobs/web/authorization.js +2 -33
- package/build/background-jobs/web/path-matcher.js +5 -30
- package/build/configuration-types.js +17 -0
- package/build/configuration.js +70 -0
- package/build/database/drivers/base.js +17 -0
- package/build/database/drivers/mssql/index.js +30 -0
- package/build/database/record/index.js +22 -10
- package/build/deployment-api/controller.js +437 -0
- package/build/deployment-api/index.js +210 -0
- package/build/deployment-api/path-matcher.js +45 -0
- package/build/deployment-api/registry.js +84 -0
- package/build/deployment-api/run-store.js +798 -0
- package/build/deployment-api/sanitize.js +114 -0
- package/build/http-server/client/index.js +77 -4
- package/build/http-server/client/response-compression.js +224 -0
- package/build/http-server/client/response.js +57 -0
- package/build/http-server/websocket-events-host.js +91 -48
- package/build/src/background-jobs/web/authorization.d.ts.map +1 -1
- package/build/src/background-jobs/web/authorization.js +3 -29
- package/build/src/background-jobs/web/path-matcher.d.ts +2 -12
- package/build/src/background-jobs/web/path-matcher.d.ts.map +1 -1
- package/build/src/background-jobs/web/path-matcher.js +5 -30
- package/build/src/configuration-types.d.ts +55 -0
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +16 -1
- package/build/src/configuration.d.ts +6 -0
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +61 -1
- package/build/src/database/drivers/base.d.ts +16 -0
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +16 -1
- package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mssql/index.js +29 -1
- package/build/src/database/record/index.d.ts +3 -2
- package/build/src/database/record/index.d.ts.map +1 -1
- package/build/src/database/record/index.js +21 -10
- package/build/src/deployment-api/controller.d.ts +117 -0
- package/build/src/deployment-api/controller.d.ts.map +1 -0
- package/build/src/deployment-api/controller.js +384 -0
- package/build/src/deployment-api/index.d.ts +46 -0
- package/build/src/deployment-api/index.d.ts.map +1 -0
- package/build/src/deployment-api/index.js +178 -0
- package/build/src/deployment-api/path-matcher.d.ts +31 -0
- package/build/src/deployment-api/path-matcher.d.ts.map +1 -0
- package/build/src/deployment-api/path-matcher.js +39 -0
- package/build/src/deployment-api/registry.d.ts +106 -0
- package/build/src/deployment-api/registry.d.ts.map +1 -0
- package/build/src/deployment-api/registry.js +74 -0
- package/build/src/deployment-api/run-store.d.ts +402 -0
- package/build/src/deployment-api/run-store.d.ts.map +1 -0
- package/build/src/deployment-api/run-store.js +711 -0
- package/build/src/deployment-api/sanitize.d.ts +27 -0
- package/build/src/deployment-api/sanitize.d.ts.map +1 -0
- package/build/src/deployment-api/sanitize.js +100 -0
- package/build/src/http-server/client/index.d.ts +17 -1
- package/build/src/http-server/client/index.d.ts.map +1 -1
- package/build/src/http-server/client/index.js +74 -5
- package/build/src/http-server/client/response-compression.d.ts +71 -0
- package/build/src/http-server/client/response-compression.d.ts.map +1 -0
- package/build/src/http-server/client/response-compression.js +197 -0
- package/build/src/http-server/client/response.d.ts +27 -0
- package/build/src/http-server/client/response.d.ts.map +1 -1
- package/build/src/http-server/client/response.js +49 -1
- package/build/src/http-server/websocket-events-host.d.ts +28 -11
- package/build/src/http-server/websocket-events-host.d.ts.map +1 -1
- package/build/src/http-server/websocket-events-host.js +84 -46
- package/build/src/sync/signed-sync-envelope-replay-service.d.ts +154 -0
- package/build/src/sync/signed-sync-envelope-replay-service.d.ts.map +1 -0
- package/build/src/sync/signed-sync-envelope-replay-service.js +294 -0
- package/build/src/sync/sync-envelope-replay-service.d.ts +84 -15
- package/build/src/sync/sync-envelope-replay-service.d.ts.map +1 -1
- package/build/src/sync/sync-envelope-replay-service.js +98 -16
- package/build/src/utils/bearer-token.d.ts +15 -0
- package/build/src/utils/bearer-token.d.ts.map +1 -0
- package/build/src/utils/bearer-token.js +29 -0
- package/build/src/utils/mount-prefix.d.ts +21 -0
- package/build/src/utils/mount-prefix.d.ts.map +1 -0
- package/build/src/utils/mount-prefix.js +35 -0
- package/build/sync/signed-sync-envelope-replay-service.js +342 -0
- package/build/sync/sync-envelope-replay-service.js +110 -15
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/utils/bearer-token.js +34 -0
- package/build/utils/mount-prefix.js +36 -0
- package/package.json +4 -3
- package/scripts/docker-run.sh +20 -0
- package/scripts/verify-docker-dev-environment.js +579 -0
- package/src/background-jobs/web/authorization.js +2 -33
- package/src/background-jobs/web/path-matcher.js +5 -30
- package/src/configuration-types.js +17 -0
- package/src/configuration.js +70 -0
- package/src/database/drivers/base.js +17 -0
- package/src/database/drivers/mssql/index.js +30 -0
- package/src/database/record/index.js +22 -10
- package/src/deployment-api/controller.js +437 -0
- package/src/deployment-api/index.js +210 -0
- package/src/deployment-api/path-matcher.js +45 -0
- package/src/deployment-api/registry.js +84 -0
- package/src/deployment-api/run-store.js +798 -0
- package/src/deployment-api/sanitize.js +114 -0
- package/src/http-server/client/index.js +77 -4
- package/src/http-server/client/response-compression.js +224 -0
- package/src/http-server/client/response.js +57 -0
- package/src/http-server/websocket-events-host.js +91 -48
- package/src/sync/signed-sync-envelope-replay-service.js +342 -0
- package/src/sync/sync-envelope-replay-service.js +110 -15
- package/src/utils/bearer-token.js +34 -0
- package/src/utils/mount-prefix.js +36 -0
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* Declarative client sync scopes with per-scope cursors, automatic mutation tracking, realtime delivery, and `sync`/`pull` progress reporting for "X of Y" import screens (see [docs/sync-client.md](docs/sync-client.md))
|
|
16
16
|
* Reactive `useLiveQuery(Model.where(...))` queries that stay current from committed local model changes across local writes, pulls, and realtime (see [docs/live-queries.md](docs/live-queries.md))
|
|
17
17
|
* Server-side sync envelope replay orchestration for app-owned sync receivers (see [docs/sync-envelope-replay-service.md](docs/sync-envelope-replay-service.md))
|
|
18
|
+
* AwesomeTasks-shaped offline sync proof using routed resources, domain commands, signed offline grants, and peer-forwarded mutations (see [docs/awesome-tasks-offline-sync-proof.md](docs/awesome-tasks-offline-sync-proof.md))
|
|
18
19
|
* SQLite web persistence that automatically prefers OPFS, then IndexedDB, and migrates legacy persisted bytes when possible (see [docs/sqlite-web-persistence.md](docs/sqlite-web-persistence.md))
|
|
19
20
|
* Expo / Metro compatibility guidance and a real Expo export check (see [docs/expo-metro-compatibility.md](docs/expo-metro-compatibility.md))
|
|
20
21
|
* Gap-less positional lists with automatic reordering via `actsAsList`, including models with numeric, string, or UUID primary keys (see [docs/acts-as-list.md](docs/acts-as-list.md))
|
|
@@ -26,8 +27,10 @@
|
|
|
26
27
|
* Translated model attributes with current-locale relationship sorting (see [docs/translations.md](docs/translations.md))
|
|
27
28
|
* Cross-process broadcast bus for `broadcastToChannel` via `velocious beacon`, including background job runner processes (see [docs/beacon.md](docs/beacon.md))
|
|
28
29
|
* Configurable HTTP server worker handlers plus backpressured, descriptor-only file responses with completion callbacks (see [docs/http-server.md](docs/http-server.md))
|
|
30
|
+
* Default-on buffered HTTP response compression with Brotli/gzip content negotiation, global and per-response opt-outs, and HEAD-correct representation headers (see [docs/http-server.md](docs/http-server.md#response-compression))
|
|
29
31
|
* Background jobs with failure events for production reporting and authorized database-scoped dashboard count snapshots/deltas (see [docs/background-jobs.md](docs/background-jobs.md) and [docs/background-jobs-dashboard.md](docs/background-jobs-dashboard.md))
|
|
30
32
|
* Durable one-off background-job scheduling with exact epoch timestamps (see [docs/scheduled-background-job-enqueue.md](docs/scheduled-background-job-enqueue.md))
|
|
33
|
+
* Mountable authenticated callable deployment API with allowlisted projects/stages, immutable-revision validation, idempotent runs, duplicate-safe interruption reconciliation, and adapter-owned execution for integrations like Rampway (see [docs/deployment-api.md](docs/deployment-api.md))
|
|
31
34
|
* Rails-style request and database query logging (see [docs/logging.md](docs/logging.md))
|
|
32
35
|
* EJS-backed mailers with delivery, queueing, and payload rendering support (see [docs/mailers.md](docs/mailers.md))
|
|
33
36
|
* Trusted reverse proxy handling for `request.remoteAddress()` (see [docs/trusted-proxies.md](docs/trusted-proxies.md))
|
|
@@ -93,6 +96,45 @@ npm run test:expo
|
|
|
93
96
|
|
|
94
97
|
Maintainers cutting a package release must follow the [Velocious release runbook](docs/releasing.md); `npm run release:patch` commits, pushes, and publishes rather than acting as a local-only version command.
|
|
95
98
|
|
|
99
|
+
# Docker development environment
|
|
100
|
+
|
|
101
|
+
The checked-in root `Dockerfile` and `compose.yml` define one canonical `dev` service used by humans, CI, and agent systems alike (see [docs/docker-development-environment.md](docs/docker-development-environment.md)). The image is Ubuntu 26.04 LTS (pinned by digest) with Node.js 24.x from signed NodeSource, the universal apt coding/debugging baseline, and the newest published provider CLIs; it is source-independent — no project source is copied and no project dependencies are installed at image build time.
|
|
102
|
+
|
|
103
|
+
Prerequisites: Docker with the Compose v2 plugin, and this repository checked out at `$DEV_HOME_PATH/velocious` (default `DEV_HOME_PATH`: `/home/dev`).
|
|
104
|
+
|
|
105
|
+
First-use setup: copy `.env.example` to the git-ignored `.env` and set `GH_CONFIG_SOURCE_PATH` to an existing host GitHub CLI config directory:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
cp .env.example .env
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`$DEV_HOME_PATH` must be a dedicated development home that already exists, holds no credentials or secrets, and is owned by (or at least writable by) UID/GID 1000 — the in-container `dev` user. Do not point it at a general host home directory, and do not recursively chown an existing home; the external environment owns safe initial provisioning.
|
|
112
|
+
|
|
113
|
+
Normal usage:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
docker compose up --build --detach dev
|
|
117
|
+
docker compose exec dev bash
|
|
118
|
+
scripts/docker-run.sh npm ci # one-off command in a disposable container
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The dev service preserves the complete `$DEV_HOME_PATH` bind at `/home/dev`, so dependencies, caches, settings, and `node_modules` persist naturally across runs. Install dependencies with the normal package commands inside the service (for example `docker compose exec dev npm ci`), never at image build time.
|
|
122
|
+
|
|
123
|
+
Concurrent isolated instances use the standard Compose project-name contract plus a distinct development home per instance:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
COMPOSE_PROJECT_NAME=velocious-review DEV_HOME_PATH=/srv/dev-homes/review \
|
|
127
|
+
docker compose up --build --detach dev
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
GitHub CLI authentication is the sole authorized credential boundary: the host config directory named by `GH_CONFIG_SOURCE_PATH` is mounted read-only at `/home/dev/.config/gh`, with container-side `GH_CONFIG_DIR` pointing there. Do not add SSH keys or other credential mounts. Kimi (and other provider) credentials are intentionally kept out of the tracked Compose files — they are an external operational override layered on by the calling environment. Threadwire is not installed in the image or the project; it remains parent orchestration resolved through unversioned `npx` outside the container.
|
|
131
|
+
|
|
132
|
+
After changing the Docker artifacts, run the checked-in static contract verifier:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
npm run verify:docker-dev-environment
|
|
136
|
+
```
|
|
137
|
+
|
|
96
138
|
# Code quality (fallow)
|
|
97
139
|
|
|
98
140
|
[fallow](https://github.com/fallow-rs/fallow) analyzes the codebase for unused/dead code, duplication, and complexity. CI runs it as a **regression gate**: it fails only on findings beyond the committed baseline in `fallow-baselines/`, so existing backlog never blocks a PR but new issues do.
|
|
@@ -1831,6 +1873,8 @@ this.getConfiguration().getWebsocketEvents().publish(channel, payload)
|
|
|
1831
1873
|
this.renderJsonArg({status: "published"})
|
|
1832
1874
|
```
|
|
1833
1875
|
|
|
1876
|
+
Publishes are queued per channel: events on the same channel are persisted and dispatched in FIFO order, while a slow or failing channel never delays unrelated channels. `configuration.awaitPendingBroadcasts()` settles once every broadcast accepted before the call has settled. See [docs/websocket-channels.md](docs/websocket-channels.md#publish-ordering-and-failure-semantics) for the full ordering and failure contract.
|
|
1877
|
+
|
|
1834
1878
|
## Websocket channels
|
|
1835
1879
|
|
|
1836
1880
|
You can resolve websocket channel classes from subscribe messages and let them decide which streams to allow:
|
|
@@ -2365,6 +2409,8 @@ When the server runs in the `development` environment, Velocious watches applica
|
|
|
2365
2409
|
|
|
2366
2410
|
Starting the HTTP server creates `tmp/server.lock` under the configured application directory before Beacon, workers, or the TCP listener start. A second server for the same app fails fast with the lock owner details instead of partially starting. Normal shutdown removes the lock; stale locks with a dead local PID are reclaimed automatically, while locks from another host or unreadable metadata should be removed manually only after confirming no server is running. See [docs/http-server.md](docs/http-server.md#server-lock).
|
|
2367
2411
|
|
|
2412
|
+
Buffered string and `Uint8Array` responses are compressed with Brotli (`br`) or gzip by default whenever request negotiation and response eligibility allow — no opt-in is required. Disable compression globally with `httpServer.compression: false` or `httpServer.compression: {enabled: false}`, and tune it with `threshold`/`brotliQuality`/`gzipLevel` overrides. Negotiation honors `Accept-Encoding` q-values, wildcards, and identity semantics (empty `406` when no acceptable representation exists), merges `Accept-Encoding` into `Vary`, and skips streamed `sendFile` responses, already-encoded or `no-transform` responses, server-sent events, partial/range responses, bodyless statuses, and non-allowlisted content types. Transformation is additionally excluded automatically for credentialed traffic and validator-carrying responses — requests with `Authorization`/`Cookie` and responses with `Set-Cookie`, `ETag`, `Digest`, or `Content-Digest` are never compressed (compression-oracle protection, and validators stay application-owned). Controllers opt out per response with `response.disableCompression()`, and HEAD requests compute GET-equivalent representation headers without emitting a body. See [docs/http-server.md](docs/http-server.md#response-compression).
|
|
2413
|
+
|
|
2368
2414
|
# Authorization (CanCan-style)
|
|
2369
2415
|
|
|
2370
2416
|
Define resource classes with an `abilities()` method and use `can` / `cannot` rules to constrain model access.
|
|
@@ -1,37 +1,6 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Constant-time comparison so token checks don't leak length/contents through
|
|
7
|
-
* timing. Returns false for differing lengths before the timing-safe compare.
|
|
8
|
-
* @param {string} a - First value.
|
|
9
|
-
* @param {string} b - Second value.
|
|
10
|
-
* @returns {boolean} - Whether the values are equal.
|
|
11
|
-
*/
|
|
12
|
-
function safeEqual(a, b) {
|
|
13
|
-
const bufferA = Buffer.from(String(a))
|
|
14
|
-
const bufferB = Buffer.from(String(b))
|
|
15
|
-
|
|
16
|
-
if (bufferA.length !== bufferB.length) return false
|
|
17
|
-
|
|
18
|
-
return crypto.timingSafeEqual(bufferA, bufferB)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Runs bearer token.
|
|
23
|
-
* @param {import("../../http-server/client/request.js").default | import("../../http-server/client/websocket-request.js").default} request - Request object.
|
|
24
|
-
* @returns {string | null} - Bearer token from the Authorization header, if any.
|
|
25
|
-
*/
|
|
26
|
-
function bearerToken(request) {
|
|
27
|
-
const header = request.header("authorization")
|
|
28
|
-
|
|
29
|
-
if (typeof header !== "string") return null
|
|
30
|
-
|
|
31
|
-
const match = header.match(/^Bearer\s+(.+)$/i)
|
|
32
|
-
|
|
33
|
-
return match ? match[1].trim() : null
|
|
34
|
-
}
|
|
3
|
+
import {bearerToken, constantTimeEqual} from "../../utils/bearer-token.js"
|
|
35
4
|
|
|
36
5
|
/**
|
|
37
6
|
* Runs is loopback.
|
|
@@ -72,7 +41,7 @@ export async function authorizeJobsRequest({ability, configuration, options, req
|
|
|
72
41
|
|
|
73
42
|
if (accessTokens.length > 0 && token) {
|
|
74
43
|
for (const accessToken of accessTokens) {
|
|
75
|
-
if (
|
|
44
|
+
if (constantTimeEqual(token, accessToken)) return true
|
|
76
45
|
}
|
|
77
46
|
}
|
|
78
47
|
|
|
@@ -1,28 +1,13 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
+
import {mountSubPath, normalizeMountPrefix} from "../../utils/mount-prefix.js"
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* @typedef {object} JobsApiMatch
|
|
5
7
|
* @property {string} action - Controller action to run.
|
|
6
8
|
* @property {Record<string, string>} params - Extra params extracted from the path.
|
|
7
9
|
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Normalizes a mount prefix: ensures a leading slash and strips any trailing
|
|
11
|
-
* slash so `/velocious/jobs/` and `/velocious/jobs` behave identically.
|
|
12
|
-
* @param {string} at - Raw mount prefix.
|
|
13
|
-
* @returns {string} - Normalized prefix.
|
|
14
|
-
*/
|
|
15
|
-
export function normalizeMountPrefix(at) {
|
|
16
|
-
if (typeof at !== "string" || !at.startsWith("/")) {
|
|
17
|
-
throw new Error(`mount requires an 'at' path starting with '/', got: ${String(at)}`)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
if (at.length > 1 && at.endsWith("/")) {
|
|
21
|
-
return at.slice(0, -1)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return at
|
|
25
|
-
}
|
|
10
|
+
export {normalizeMountPrefix}
|
|
26
11
|
|
|
27
12
|
/**
|
|
28
13
|
* Matches an incoming request against the read-only jobs API routes that live
|
|
@@ -35,19 +20,9 @@ export function normalizeMountPrefix(at) {
|
|
|
35
20
|
* @returns {JobsApiMatch | null} - Matched action or null.
|
|
36
21
|
*/
|
|
37
22
|
export function matchJobsApiPath({prefix, path, method}) {
|
|
38
|
-
|
|
39
|
-
let subPath
|
|
23
|
+
const subPath = mountSubPath({prefix, path})
|
|
40
24
|
|
|
41
|
-
if (
|
|
42
|
-
// Root mount: the whole path is the sub-path (avoid building a "//" guard).
|
|
43
|
-
subPath = path
|
|
44
|
-
} else if (path === prefix) {
|
|
45
|
-
subPath = "/"
|
|
46
|
-
} else if (path.startsWith(`${prefix}/`)) {
|
|
47
|
-
subPath = path.slice(prefix.length)
|
|
48
|
-
} else {
|
|
49
|
-
return null
|
|
50
|
-
}
|
|
25
|
+
if (subPath === null) return null
|
|
51
26
|
|
|
52
27
|
if (method === "GET" && subPath === "/api/health") return {action: "health", params: {}}
|
|
53
28
|
if (method === "GET" && subPath === "/api/stats") return {action: "stats", params: {}}
|
|
@@ -249,8 +249,25 @@
|
|
|
249
249
|
* @property {number} [unreachableReportMs] - Grace window (ms) a beacon connect/disconnect blip must persist before it is reported as a framework-error. Transient outages that recover within this window (e.g. a deploy restarting the broker) are not reported. Defaults to 30000.
|
|
250
250
|
*/
|
|
251
251
|
|
|
252
|
+
/**
|
|
253
|
+
* @typedef {object} HttpCompressionConfiguration
|
|
254
|
+
* @property {boolean} [enabled] - Whether buffered response compression is enabled. Defaults to true; set false to disable globally.
|
|
255
|
+
* @property {number} [threshold] - Minimum buffered body size in bytes before compression is applied. Defaults to 1024.
|
|
256
|
+
* @property {number} [brotliQuality] - Brotli encoder quality (0-11). Defaults to 4.
|
|
257
|
+
* @property {number} [gzipLevel] - Gzip compression level (0-9). Defaults to 6.
|
|
258
|
+
*/
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* @typedef {object} NormalizedHttpCompressionConfiguration
|
|
262
|
+
* @property {boolean} enabled - Whether buffered HTTP response compression is enabled.
|
|
263
|
+
* @property {number} threshold - Minimum buffered body size in bytes before compression is applied.
|
|
264
|
+
* @property {number} brotliQuality - Brotli encoder quality (0-11).
|
|
265
|
+
* @property {number} gzipLevel - Gzip compression level (0-9).
|
|
266
|
+
*/
|
|
267
|
+
|
|
252
268
|
/**
|
|
253
269
|
* @typedef {object} HttpServerConfiguration
|
|
270
|
+
* @property {boolean | HttpCompressionConfiguration} [compression] - Buffered response compression. Enabled with documented defaults when absent; false or {enabled: false} disables it globally.
|
|
254
271
|
* @property {string} [host] - Hostname to bind the HTTP server to.
|
|
255
272
|
* @property {boolean} [inProcess] - Run HTTP handlers in the main thread instead of worker threads.
|
|
256
273
|
* @property {number} [maxWorkers] - Backward-compatible alias for workers.
|
package/build/configuration.js
CHANGED
|
@@ -120,6 +120,10 @@ const DEFAULT_WEBSOCKET_INBOUND_MAX_PENDING_MESSAGES = 256
|
|
|
120
120
|
const DEFAULT_WEBSOCKET_OUTBOUND_MAX_PENDING_BYTES = 16 * 1024 * 1024
|
|
121
121
|
const DEFAULT_WEBSOCKET_OUTBOUND_MAX_PENDING_FRAMES = 256
|
|
122
122
|
|
|
123
|
+
const DEFAULT_COMPRESSION_THRESHOLD = 1024
|
|
124
|
+
const DEFAULT_COMPRESSION_BROTLI_QUALITY = 4
|
|
125
|
+
const DEFAULT_COMPRESSION_GZIP_LEVEL = 6
|
|
126
|
+
|
|
123
127
|
/**
|
|
124
128
|
* Validates a positive safe integer configuration value.
|
|
125
129
|
* @param {?} value - Configured positive safe integer.
|
|
@@ -136,6 +140,63 @@ function positiveSafeInteger(value, name, defaultValue) {
|
|
|
136
140
|
return value
|
|
137
141
|
}
|
|
138
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Validates an integer configuration value inside an inclusive range.
|
|
145
|
+
* @param {?} value - Configured integer.
|
|
146
|
+
* @param {string} name - Configuration key.
|
|
147
|
+
* @param {number} min - Minimum accepted value (inclusive).
|
|
148
|
+
* @param {number} max - Maximum accepted value (inclusive).
|
|
149
|
+
* @param {number} defaultValue - Default value.
|
|
150
|
+
* @returns {number} - Validated configured or default value.
|
|
151
|
+
*/
|
|
152
|
+
function integerInRange(value, name, min, max, defaultValue) {
|
|
153
|
+
if (value === undefined) return defaultValue
|
|
154
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max) {
|
|
155
|
+
throw new TypeError(`${name} must be an integer between ${min} and ${max}`)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return value
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Normalizes the buffered HTTP response compression configuration. Compression is
|
|
163
|
+
* enabled by default when the setting is absent; `false` or `{enabled: false}`
|
|
164
|
+
* disables it globally.
|
|
165
|
+
* @param {boolean | import("./configuration-types.js").HttpCompressionConfiguration | undefined} value - Configured compression value.
|
|
166
|
+
* @returns {import("./configuration-types.js").NormalizedHttpCompressionConfiguration} - Normalized compression configuration.
|
|
167
|
+
*/
|
|
168
|
+
function normalizeHttpCompression(value) {
|
|
169
|
+
if (value === undefined || value === true) {
|
|
170
|
+
return {enabled: true, threshold: DEFAULT_COMPRESSION_THRESHOLD, brotliQuality: DEFAULT_COMPRESSION_BROTLI_QUALITY, gzipLevel: DEFAULT_COMPRESSION_GZIP_LEVEL}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (value === false) {
|
|
174
|
+
return {enabled: false, threshold: DEFAULT_COMPRESSION_THRESHOLD, brotliQuality: DEFAULT_COMPRESSION_BROTLI_QUALITY, gzipLevel: DEFAULT_COMPRESSION_GZIP_LEVEL}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
178
|
+
throw new TypeError(`httpServer.compression must be a boolean or an object, got: ${String(value)}`)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const {brotliQuality, enabled, gzipLevel, threshold, ...restCompression} = value
|
|
182
|
+
const restCompressionKeys = Object.keys(restCompression)
|
|
183
|
+
|
|
184
|
+
if (restCompressionKeys.length > 0) {
|
|
185
|
+
throw new TypeError(`httpServer.compression received unknown keys: ${restCompressionKeys.join(", ")} (supported: brotliQuality, enabled, gzipLevel, threshold)`)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (enabled !== undefined && typeof enabled !== "boolean") {
|
|
189
|
+
throw new TypeError(`httpServer.compression.enabled must be a boolean, got: ${String(enabled)}`)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
enabled: enabled ?? true,
|
|
194
|
+
threshold: positiveSafeInteger(threshold, "httpServer.compression.threshold", DEFAULT_COMPRESSION_THRESHOLD),
|
|
195
|
+
brotliQuality: integerInRange(brotliQuality, "httpServer.compression.brotliQuality", 0, 11, DEFAULT_COMPRESSION_BROTLI_QUALITY),
|
|
196
|
+
gzipLevel: integerInRange(gzipLevel, "httpServer.compression.gzipLevel", 0, 9, DEFAULT_COMPRESSION_GZIP_LEVEL)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
139
200
|
export default class VelociousConfiguration {
|
|
140
201
|
/**
|
|
141
202
|
* Close database connections promise.
|
|
@@ -236,6 +297,7 @@ export default class VelociousConfiguration {
|
|
|
236
297
|
|
|
237
298
|
this.httpServer = {
|
|
238
299
|
...(httpServer || {}),
|
|
300
|
+
compression: normalizeHttpCompression(httpServer?.compression),
|
|
239
301
|
websocketInboundQueue: {
|
|
240
302
|
maxPendingBytes: positiveSafeInteger(websocketInboundQueue?.maxPendingBytes, "httpServer.websocketInboundQueue.maxPendingBytes", DEFAULT_WEBSOCKET_INBOUND_MAX_PENDING_BYTES),
|
|
241
303
|
maxPendingMessages: positiveSafeInteger(websocketInboundQueue?.maxPendingMessages, "httpServer.websocketInboundQueue.maxPendingMessages", DEFAULT_WEBSOCKET_INBOUND_MAX_PENDING_MESSAGES)
|
|
@@ -500,6 +562,14 @@ export default class VelociousConfiguration {
|
|
|
500
562
|
return this.cors
|
|
501
563
|
}
|
|
502
564
|
|
|
565
|
+
/**
|
|
566
|
+
* Runs get http server compression.
|
|
567
|
+
* @returns {import("./configuration-types.js").NormalizedHttpCompressionConfiguration} - Normalized buffered response compression configuration.
|
|
568
|
+
*/
|
|
569
|
+
getHttpServerCompression() {
|
|
570
|
+
return this.httpServer.compression
|
|
571
|
+
}
|
|
572
|
+
|
|
503
573
|
/**
|
|
504
574
|
* Runs get cookie secret.
|
|
505
575
|
* @returns {string | undefined} - Cookie secret.
|
|
@@ -961,6 +961,23 @@ export default class VelociousDatabaseDriversBase {
|
|
|
961
961
|
*/
|
|
962
962
|
supportsDefaultPrimaryKeyUUID() { return false }
|
|
963
963
|
|
|
964
|
+
/**
|
|
965
|
+
* Executes an insert that carries an explicit primary-key value
|
|
966
|
+
* (client-generated offline-sync ids). Drivers whose auto-increment columns
|
|
967
|
+
* reject explicit values (MSSQL IDENTITY) override this to run the insert
|
|
968
|
+
* with identity insert enabled in a single request.
|
|
969
|
+
* @param {object} args - Options object.
|
|
970
|
+
* @param {QueryOptions} args.options - Query options for the standard query path.
|
|
971
|
+
* @param {string} args.sql - Generated insert SQL.
|
|
972
|
+
* @param {string} args.tableName - Table being inserted into.
|
|
973
|
+
* @returns {Promise<QueryResultType>} - Insert result.
|
|
974
|
+
*/
|
|
975
|
+
async insertWithExplicitPrimaryKey({options, sql, tableName}) {
|
|
976
|
+
void tableName
|
|
977
|
+
|
|
978
|
+
return await this.query(sql, options)
|
|
979
|
+
}
|
|
980
|
+
|
|
964
981
|
/**
|
|
965
982
|
* Runs supports insert into returning.
|
|
966
983
|
* @abstract
|
|
@@ -362,6 +362,36 @@ export default class VelociousDatabaseDriversMssql extends Base{
|
|
|
362
362
|
shouldSetAutoIncrementWhenPrimaryKey() { return true }
|
|
363
363
|
supportsDefaultPrimaryKeyUUID() { return true }
|
|
364
364
|
|
|
365
|
+
/**
|
|
366
|
+
* Runs an explicit primary-key insert as one batch request: SQL Server scopes
|
|
367
|
+
* IDENTITY_INSERT to the session, and node-mssql pool-backed requests may use
|
|
368
|
+
* a different physical session per query, so enabling it in a separate query
|
|
369
|
+
* can leave the actual INSERT on another session. A single batch keeps the
|
|
370
|
+
* whole sequence on one session by construction: enable, insert, disable on
|
|
371
|
+
* success, and a CATCH that disables and rethrows the original error.
|
|
372
|
+
* @param {object} args - Options object.
|
|
373
|
+
* @param {import("../base.js").QueryOptions} args.options - Query options for the standard query path.
|
|
374
|
+
* @param {string} args.sql - Generated insert SQL.
|
|
375
|
+
* @param {string} args.tableName - Table being inserted into.
|
|
376
|
+
* @returns {Promise<import("../base.js").QueryResultType>} - Insert result.
|
|
377
|
+
*/
|
|
378
|
+
async insertWithExplicitPrimaryKey({options, sql, tableName}) {
|
|
379
|
+
const quotedTable = this.quoteTable(tableName)
|
|
380
|
+
const batch = [
|
|
381
|
+
`SET IDENTITY_INSERT ${quotedTable} ON;`,
|
|
382
|
+
"BEGIN TRY",
|
|
383
|
+
`${sql};`,
|
|
384
|
+
`SET IDENTITY_INSERT ${quotedTable} OFF;`,
|
|
385
|
+
"END TRY",
|
|
386
|
+
"BEGIN CATCH",
|
|
387
|
+
`SET IDENTITY_INSERT ${quotedTable} OFF;`,
|
|
388
|
+
"THROW;",
|
|
389
|
+
"END CATCH"
|
|
390
|
+
].join("\n")
|
|
391
|
+
|
|
392
|
+
return await this.query(batch, options)
|
|
393
|
+
}
|
|
394
|
+
|
|
365
395
|
/**
|
|
366
396
|
* Runs escape.
|
|
367
397
|
* @param {?} value - Value to use.
|
|
@@ -4248,15 +4248,21 @@ class VelociousDatabaseRecord {
|
|
|
4248
4248
|
* @returns {Promise<void>} - Resolves when complete.
|
|
4249
4249
|
*/
|
|
4250
4250
|
async _createNewRecord() {
|
|
4251
|
-
|
|
4252
|
-
|
|
4251
|
+
// Resolve the connection once and pin the whole insert path to it: a pool
|
|
4252
|
+
// can resolve a different current connection across the awaits below, and
|
|
4253
|
+
// the identity-insert wrapper is only effective on the exact session that
|
|
4254
|
+
// ran SET IDENTITY_INSERT.
|
|
4255
|
+
const connection = this._connection()
|
|
4256
|
+
|
|
4257
|
+
if (!connection["insertSql"]) {
|
|
4258
|
+
throw new Error(`No insertSql on ${connection.constructor.name}`)
|
|
4253
4259
|
}
|
|
4254
4260
|
|
|
4255
4261
|
const data = Object.assign({}, this._belongsToChanges(), this.rawAttributes())
|
|
4256
4262
|
const primaryKey = this.getModelClass().primaryKey()
|
|
4257
4263
|
const primaryKeyColumn = this.getModelClass().getColumns().find((column) => column.getName() == primaryKey)
|
|
4258
4264
|
const primaryKeyType = primaryKeyColumn?.getType()?.toLowerCase()
|
|
4259
|
-
const driverSupportsDefaultUUID = typeof
|
|
4265
|
+
const driverSupportsDefaultUUID = typeof connection.supportsDefaultPrimaryKeyUUID == "function" && connection.supportsDefaultPrimaryKeyUUID()
|
|
4260
4266
|
const isUUIDPrimaryKey = primaryKeyType?.includes("uuid")
|
|
4261
4267
|
const shouldAssignUUIDPrimaryKey = isUUIDPrimaryKey && !driverSupportsDefaultUUID
|
|
4262
4268
|
this._setDefaultTimestampValues(data)
|
|
@@ -4270,14 +4276,20 @@ class VelociousDatabaseRecord {
|
|
|
4270
4276
|
|
|
4271
4277
|
this._normalizeDateValuesForWrite(data)
|
|
4272
4278
|
|
|
4273
|
-
const sql =
|
|
4279
|
+
const sql = connection.insertSql({
|
|
4274
4280
|
returnLastInsertedColumnNames: columnNames,
|
|
4275
4281
|
tableName: this._tableName(),
|
|
4276
4282
|
data
|
|
4277
4283
|
})
|
|
4278
|
-
const
|
|
4279
|
-
|
|
4280
|
-
|
|
4284
|
+
const insertOptions = {logName: `${this.getModelClass().name} Create`}
|
|
4285
|
+
// Explicit primary-key inserts into auto-increment columns go through the
|
|
4286
|
+
// driver's explicit-primary-key insert (MSSQL wraps it in IDENTITY_INSERT);
|
|
4287
|
+
// everything else uses the plain query path.
|
|
4288
|
+
const insertResult = hasUserProvidedPrimaryKey && primaryKeyColumn?.getAutoIncrement() === true
|
|
4289
|
+
? await connection.insertWithExplicitPrimaryKey({options: insertOptions, sql, tableName: this._tableName()})
|
|
4290
|
+
: await connection.query(sql, insertOptions)
|
|
4291
|
+
|
|
4292
|
+
await this._applyInsertResult({connection, data, insertResult, primaryKey})
|
|
4281
4293
|
this.setIsNewRecord(false)
|
|
4282
4294
|
|
|
4283
4295
|
this._markLoadedRelationshipsPreloadedAfterCreate()
|
|
@@ -4303,10 +4315,10 @@ class VelociousDatabaseRecord {
|
|
|
4303
4315
|
|
|
4304
4316
|
/**
|
|
4305
4317
|
* Applies the database insert response to this record.
|
|
4306
|
-
* @param {{data: Record<string, string | number | boolean | Date | null | undefined>, insertResult: Array<Record<string, string | number | boolean | Date | null | undefined>> | null | undefined, primaryKey: string}} options -
|
|
4318
|
+
* @param {{connection: import("../drivers/base.js").default, data: Record<string, string | number | boolean | Date | null | undefined>, insertResult: Array<Record<string, string | number | boolean | Date | null | undefined>> | null | undefined, primaryKey: string}} options - Pinned insert connection, inserted data, connection result, and primary key column name.
|
|
4307
4319
|
* @returns {Promise<void>} - Resolves when complete.
|
|
4308
4320
|
*/
|
|
4309
|
-
async _applyInsertResult({data, insertResult, primaryKey}) {
|
|
4321
|
+
async _applyInsertResult({connection, data, insertResult, primaryKey}) {
|
|
4310
4322
|
if (Array.isArray(insertResult) && insertResult[0] && insertResult[0][primaryKey]) {
|
|
4311
4323
|
this._attributes = insertResult[0]
|
|
4312
4324
|
this._changes = {}
|
|
@@ -4322,7 +4334,7 @@ class VelociousDatabaseRecord {
|
|
|
4322
4334
|
return
|
|
4323
4335
|
}
|
|
4324
4336
|
|
|
4325
|
-
const id = await
|
|
4337
|
+
const id = await connection.lastInsertID()
|
|
4326
4338
|
|
|
4327
4339
|
await this._reloadWithId(id)
|
|
4328
4340
|
}
|