velocious 1.0.576 → 1.0.578
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 +57 -6
- package/build/background-jobs/main.js +15 -4
- package/build/background-jobs/scheduler.js +11 -7
- package/build/background-jobs/store.js +18 -13
- package/build/configuration-types.js +1 -0
- package/build/configuration.js +52 -10
- package/build/database/drivers/mysql/structure-sql.js +70 -9
- package/build/database/query/with-count.js +8 -8
- package/build/database/record/record-not-found-error.js +1 -0
- package/build/environment-handlers/node/cli/commands/test.js +2 -0
- package/build/environment-handlers/node/source-peer-package.js +172 -0
- package/build/frontend-model-controller.js +71 -69
- package/build/frontend-models/base.js +10 -1
- package/build/routes/resolver.js +3 -50
- package/build/src/background-jobs/main.d.ts.map +1 -1
- package/build/src/background-jobs/main.js +15 -5
- package/build/src/background-jobs/scheduler.d.ts +2 -2
- package/build/src/background-jobs/scheduler.d.ts.map +1 -1
- package/build/src/background-jobs/scheduler.js +11 -8
- package/build/src/background-jobs/store.d.ts +5 -4
- package/build/src/background-jobs/store.d.ts.map +1 -1
- package/build/src/background-jobs/store.js +17 -12
- 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 +13 -1
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +52 -11
- package/build/src/database/drivers/mysql/structure-sql.d.ts +11 -0
- package/build/src/database/drivers/mysql/structure-sql.d.ts.map +1 -1
- package/build/src/database/drivers/mysql/structure-sql.js +61 -10
- package/build/src/database/query/with-count.js +8 -8
- package/build/src/database/record/record-not-found-error.d.ts +1 -0
- package/build/src/database/record/record-not-found-error.d.ts.map +1 -1
- package/build/src/database/record/record-not-found-error.js +2 -1
- package/build/src/environment-handlers/node/cli/commands/test.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/test.js +3 -1
- package/build/src/environment-handlers/node/source-peer-package.d.ts +46 -0
- package/build/src/environment-handlers/node/source-peer-package.d.ts.map +1 -0
- package/build/src/environment-handlers/node/source-peer-package.js +148 -0
- package/build/src/frontend-model-controller.d.ts +10 -10
- package/build/src/frontend-model-controller.d.ts.map +1 -1
- package/build/src/frontend-model-controller.js +73 -67
- package/build/src/frontend-models/base.d.ts.map +1 -1
- package/build/src/frontend-models/base.js +11 -2
- package/build/src/routes/resolver.d.ts.map +1 -1
- package/build/src/routes/resolver.js +4 -45
- package/build/src/velocious-error.d.ts +10 -0
- package/build/src/velocious-error.d.ts.map +1 -1
- package/build/src/velocious-error.js +8 -2
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/velocious-error.js +7 -1
- package/package.json +3 -2
- package/scripts/run-tests.js +61 -20
- package/scripts/test-browser.js +6 -1
- package/src/background-jobs/main.js +15 -4
- package/src/background-jobs/scheduler.js +11 -7
- package/src/background-jobs/store.js +18 -13
- package/src/configuration-types.js +1 -0
- package/src/configuration.js +52 -10
- package/src/database/drivers/mysql/structure-sql.js +70 -9
- package/src/database/query/with-count.js +8 -8
- package/src/database/record/record-not-found-error.js +1 -0
- package/src/environment-handlers/node/cli/commands/test.js +2 -0
- package/src/environment-handlers/node/source-peer-package.js +172 -0
- package/src/frontend-model-controller.js +71 -69
- package/src/frontend-models/base.js +10 -1
- package/src/routes/resolver.js +3 -50
- package/src/velocious-error.js +7 -1
- package/build/deployment-api/controller.js +0 -437
- package/build/deployment-api/index.js +0 -210
- package/build/deployment-api/path-matcher.js +0 -45
- package/build/deployment-api/registry.js +0 -84
- package/build/deployment-api/run-store.js +0 -798
- package/build/deployment-api/sanitize.js +0 -114
- package/build/src/deployment-api/controller.d.ts +0 -117
- package/build/src/deployment-api/controller.d.ts.map +0 -1
- package/build/src/deployment-api/controller.js +0 -384
- package/build/src/deployment-api/index.d.ts +0 -46
- package/build/src/deployment-api/index.d.ts.map +0 -1
- package/build/src/deployment-api/index.js +0 -178
- package/build/src/deployment-api/path-matcher.d.ts +0 -31
- package/build/src/deployment-api/path-matcher.d.ts.map +0 -1
- package/build/src/deployment-api/path-matcher.js +0 -39
- package/build/src/deployment-api/registry.d.ts +0 -106
- package/build/src/deployment-api/registry.d.ts.map +0 -1
- package/build/src/deployment-api/registry.js +0 -74
- package/build/src/deployment-api/run-store.d.ts +0 -402
- package/build/src/deployment-api/run-store.d.ts.map +0 -1
- package/build/src/deployment-api/run-store.js +0 -711
- package/build/src/deployment-api/sanitize.d.ts +0 -27
- package/build/src/deployment-api/sanitize.d.ts.map +0 -1
- package/build/src/deployment-api/sanitize.js +0 -100
- package/src/deployment-api/controller.js +0 -437
- package/src/deployment-api/index.js +0 -210
- package/src/deployment-api/path-matcher.js +0 -45
- package/src/deployment-api/registry.js +0 -84
- package/src/deployment-api/run-store.js +0 -798
- package/src/deployment-api/sanitize.js +0 -114
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* Declarative state machines for models, with typed event methods generated into the base model (see [docs/state-machine.md](docs/state-machine.md))
|
|
10
10
|
* Migrations for schema changes and UTC datetime storage (see [docs/database-migrations.md](docs/database-migrations.md))
|
|
11
11
|
* External packages (engines) that contribute data models, frontend-model resources and migrations to a consuming app (see [docs/packages.md](docs/packages.md))
|
|
12
|
+
* Optional Rampway-owned durable deployment control plane mounted through the standard routes DSL on Velocious 1.0.577 or newer (see [docs/rampway-integration.md](docs/rampway-integration.md))
|
|
12
13
|
* Controllers and views for HTTP endpoints
|
|
13
14
|
* Frontend-model transport for creating, updating, querying, and subscribing to query-filtered lifecycle events over HTTP/WebSocket, with structured per-attribute validation error responses and one-budget WebSocket startup controls (see [docs/frontend-models.md](docs/frontend-models.md) and [docs/websocket-channels.md](docs/websocket-channels.md))
|
|
14
15
|
* Client-side offline sync mutation logs and frontend-model optimistic queueing primitives (see the [shared-resource sync developer guide](docs/shared-resource-sync-guide.md) and [offline sync architecture](docs/offline-sync.md))
|
|
@@ -21,7 +22,7 @@
|
|
|
21
22
|
* 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))
|
|
22
23
|
* Rails-style nested-attribute writes on frontend-model `save()` (see [docs/nested-attributes.md](docs/nested-attributes.md))
|
|
23
24
|
* Async-aware test-data factories with inherited traits, graph-first native association autosave, metadata-aware override precedence, callbacks, sequences, and linting (see [docs/factories.md](docs/factories.md))
|
|
24
|
-
* Per-row association counts via `.withCount(...)`, including safe batching of structurally identical aggregates, on frontend and backend queries (see [docs/with-count.md](docs/with-count.md))
|
|
25
|
+
* Per-row association counts via `.withCount(...)`, including cohort-safe intersected filters and safe batching of structurally identical aggregates, on frontend and backend queries (see [docs/with-count.md](docs/with-count.md))
|
|
25
26
|
* Consumer-defined per-row SQL aggregates/computations via `.queryData(...)`, with compatible projections sharing a roundtrip while preserving declared alias-overwrite order, on frontend and backend queries (see [docs/query-data.md](docs/query-data.md))
|
|
26
27
|
* Per-record ability checks via `.abilities(...)` on frontend queries + `record.can(action)` (see [docs/abilities.md](docs/abilities.md))
|
|
27
28
|
* Translated model attributes with current-locale relationship sorting (see [docs/translations.md](docs/translations.md))
|
|
@@ -30,7 +31,6 @@
|
|
|
30
31
|
* 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))
|
|
31
32
|
* 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))
|
|
32
33
|
* 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))
|
|
34
34
|
* Rails-style request and database query logging (see [docs/logging.md](docs/logging.md))
|
|
35
35
|
* EJS-backed mailers with delivery, queueing, and payload rendering support (see [docs/mailers.md](docs/mailers.md))
|
|
36
36
|
* Trusted reverse proxy handling for `request.remoteAddress()` (see [docs/trusted-proxies.md](docs/trusted-proxies.md))
|
|
@@ -741,7 +741,7 @@ Use `await FrontendModelBase.waitForIdle()` when a test harness or app lifecycle
|
|
|
741
741
|
|
|
742
742
|
Frontend-model HTTP requests always use `credentials: "include"` so shared custom commands can set session cookies without app-level transport overrides.
|
|
743
743
|
|
|
744
|
-
Unexpected frontend-model endpoint failures stay client-safe in production with `errorMessage: "Request failed."
|
|
744
|
+
Unexpected frontend-model endpoint failures stay client-safe in production with `errorType: "internal_error"`, `errorMessage: "Request failed."`, and a server-generated `correlationId` shared with the matching framework-error report. Expected application failures can use `VelociousError.safe(message, {errorType, details, code})`; generated frontend-model callers preserve the server's safe error fields. See [docs/frontend-models.md](docs/frontend-models.md#error-payloads).
|
|
745
745
|
Invalid client query descriptors, such as unknown `select`, `where`, `search`, `joins`, `preload`, `group`, `sort`, `pluck`, or Ransack attributes, return the specific frontend-model query error message with `velocious.code: "frontend-model-query-error"` and are not emitted as framework errors.
|
|
746
746
|
Invalid frontend-model write attributes and attachment names, including attributes rejected by `permittedParams()`, return the specific safe error message with `velocious.code: "frontend-model-attribute-error"` and are not emitted as framework errors.
|
|
747
747
|
In `development` and `test`, Velocious also includes `debugErrorClass`, `debugErrorMessage`, and `debugBacktrace` fields so browser/system-test failures are easier to diagnose without exposing those details in production.
|
|
@@ -869,6 +869,51 @@ Supported route helpers:
|
|
|
869
869
|
- `routes.get(path, {to: [ControllerClass, "action"], params?})`
|
|
870
870
|
- `routes.post(path, {to: [ControllerClass, "action"], params?})`
|
|
871
871
|
|
|
872
|
+
## Rampway deployment control plane
|
|
873
|
+
|
|
874
|
+
Applications can install `rampway@^0.4.0` and mount its package-owned Velocious
|
|
875
|
+
control plane through the existing routes DSL. Keep bearer tokens in backend
|
|
876
|
+
secrets and provide explicit allowlisted config paths and release branches:
|
|
877
|
+
|
|
878
|
+
```sh
|
|
879
|
+
npm install rampway@^0.4.0 velocious@^1.0.577
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
Rampway 0.4.0 declares `velocious ^1.0.574` as its peer range, but applications
|
|
883
|
+
mounting this API must use Velocious 1.0.577 or newer. Versions 1.0.574 through
|
|
884
|
+
1.0.576 attempted an app-local controller import before the package-supplied
|
|
885
|
+
`controllerClass`, allowing a same-named app controller to shadow Rampway's
|
|
886
|
+
authenticated controller.
|
|
887
|
+
|
|
888
|
+
```js
|
|
889
|
+
import RampwayDeploymentApi from "rampway/velocious"
|
|
890
|
+
import deploymentSecrets from "./secrets/deployments.js"
|
|
891
|
+
|
|
892
|
+
routes.draw((route) => {
|
|
893
|
+
route.mount(RampwayDeploymentApi, {
|
|
894
|
+
accessTokens: deploymentSecrets.rampwayAccessTokens,
|
|
895
|
+
at: "/rampway/deployments",
|
|
896
|
+
projects: {
|
|
897
|
+
"my-app": {
|
|
898
|
+
stages: {
|
|
899
|
+
production: {
|
|
900
|
+
configPath: "/srv/my-app/control/rampway.config.mjs",
|
|
901
|
+
releaseBranch: "main"
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
},
|
|
906
|
+
workerBootstrapPath: "/srv/my-app/control/rampway-velocious-worker.mjs"
|
|
907
|
+
})
|
|
908
|
+
})
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
Rampway owns authentication, deployment execution, idempotency, durable runs,
|
|
912
|
+
audits, reconciliation, and the detached worker. Velocious supplies its normal
|
|
913
|
+
route, request, error-event, and database abstractions. See
|
|
914
|
+
[docs/rampway-integration.md](docs/rampway-integration.md) for bootstrap,
|
|
915
|
+
persistence, security, and rollback requirements.
|
|
916
|
+
|
|
872
917
|
|
|
873
918
|
```js
|
|
874
919
|
import Record from "velocious/build/src/database/record/index.js"
|
|
@@ -889,7 +934,8 @@ Translated models also get a `currentTranslation` `hasOne` relationship scoped t
|
|
|
889
934
|
|
|
890
935
|
Async class APIs initialize record metadata on first use when a model has not
|
|
891
936
|
already been initialized eagerly. See [docs/model-initialization.md](docs/model-initialization.md)
|
|
892
|
-
for the eager and lazy initialization behavior
|
|
937
|
+
for the eager and lazy initialization behavior, including atomic shared bootstrap
|
|
938
|
+
and complete recovery after an eager initialization failure.
|
|
893
939
|
|
|
894
940
|
## Lifecycle callbacks
|
|
895
941
|
|
|
@@ -1302,7 +1348,7 @@ If you need to regenerate missing structure files without rerunning migrations,
|
|
|
1302
1348
|
npx velocious db:schema:dump
|
|
1303
1349
|
```
|
|
1304
1350
|
|
|
1305
|
-
`db:schema:dump` generates a structure SQL file for each configured database identifier under `db/structure-<identifier>.sql`. It only writes files when one or more expected files are missing. The generated file includes the full DDL (tables, indexes, views, triggers, etc.) followed by `INSERT INTO schema_migrations (version) VALUES (...)` for every currently applied migration version.
|
|
1351
|
+
`db:schema:dump` generates a structure SQL file for each configured database identifier under `db/structure-<identifier>.sql`. It only writes files when one or more expected files are missing. The generated file includes the full DDL (tables, indexes, views, triggers, etc.) followed by `INSERT INTO schema_migrations (version) VALUES (...)` for every currently applied migration version. MySQL and MariaDB dumps place same-schema referenced base tables before their dependent tables. The migration ledger preserves applied versions in the checked-in snapshot so fresh databases loaded from it do not re-run migrations that already shaped the schemas in the file.
|
|
1306
1352
|
|
|
1307
1353
|
If you need to load the checked-in structure files for each configured database, use:
|
|
1308
1354
|
|
|
@@ -1812,7 +1858,7 @@ configuration.getErrorEvents().on("all-error", ({error, errorType}) => {
|
|
|
1812
1858
|
})
|
|
1813
1859
|
```
|
|
1814
1860
|
|
|
1815
|
-
Genuinely unexpected frontend-model command failures reach this bus too. The frontend-model controller catches them to return a client-safe `Request failed.`
|
|
1861
|
+
Genuinely unexpected frontend-model command failures reach this bus too. The frontend-model controller catches them to return a client-safe `internal_error` response with `Request failed.` and a correlation ID, then emits them as `framework-error`/`all-error` with the same correlation ID and `context.frontendModelEndpoint === true`. Expected user-flow errors are excluded: validation failures are forwarded with their real message (for example `Name can't be blank`), invalid client query descriptors are returned as frontend-model query errors, and `error.velocious`-annotated / `safeToExpose` errors keep their expected-error status. A raw `errorType` property alone is not considered safe and does not suppress reporting.
|
|
1816
1862
|
|
|
1817
1863
|
Unexpected inbound decoded WebSocket dispatch failures emit one `framework-error` and one matching `all-error`. Established expected client-flow errors remain excluded from both events.
|
|
1818
1864
|
|
|
@@ -2181,6 +2227,11 @@ VELOCIOUS_BACKGROUND_JOBS_JOB_TIMEOUT_MS=5400000
|
|
|
2181
2227
|
|
|
2182
2228
|
New jobs default to `executionMode: "pooled"`: a worker runs them in warm, reusable Node child runners. `pooledRunnerCount` (default: `4`) bounds this independent per-worker pool, and `pooledRunnerConcurrency` (default: `1`) sets how many jobs each child runs at once on its own event loop, so total pooled capacity is `pooledRunnerCount × pooledRunnerConcurrency` — raise concurrency for I/O-bound jobs to get high throughput from a bounded, isolated set of processes. `pooledRunnerCount`, `pooledRunnerConcurrency`, and `pooledRunnerMaxJobs` must be finite positive integers; the RSS and lifetime limits must be finite positive numbers. A child is recycled after an acknowledged job when it reaches `pooledRunnerMaxJobs` (default: `100`), `pooledRunnerMaxRssBytes` (default: `536870912`, or 512 MiB), or `pooledRunnerMaxLifetimeMs` (default: `3600000`, or one hour). `execution_mode` is the single source of truth for a job's runtime — pooled rows persist as `execution_mode = "pooled"` directly. See [execution modes and pooled runners](docs/background-jobs.md#execution-modes-and-pooled-runners).
|
|
2183
2229
|
|
|
2230
|
+
Cold pooled jobs share one atomic model-bootstrap phase. If that phase fails, all
|
|
2231
|
+
waiting jobs receive the failure; a later job in the surviving child cannot run
|
|
2232
|
+
until a complete model-initialization phase succeeds. The pool's configured
|
|
2233
|
+
concurrency and per-job connection scopes are unchanged.
|
|
2234
|
+
|
|
2184
2235
|
`maxConcurrentForkedJobs` (default: `4`) caps how many out-of-process `executionMode: "forked"` or `executionMode: "spawned"` jobs one worker may keep in flight. Forked jobs use `child_process.fork()` with an attached IPC channel. After the main process acknowledges their durable status report, forked and spawned one-shot runners exit without waiting for graceful Beacon/database teardown; the OS closes their process-owned resources. A missing or rejected status acknowledgement makes the runner exit as failed instead of reporting clean success. Spawned jobs use the legacy `background-jobs-runner` CLI process via `child_process.spawn()` and are only for callers that intentionally want that spawned behavior.
|
|
2185
2236
|
|
|
2186
2237
|
`jobTimeoutMs` (or `VELOCIOUS_BACKGROUND_JOBS_JOB_TIMEOUT_MS`, milliseconds; default: disabled) is a wall-clock backstop for `"forked"` and `"pooled"` jobs. A job still running after the timeout is terminated (`SIGTERM`, then `SIGKILL` after the reaping grace) and reported `failed`, so a genuinely-hung job can't pin a worker's capacity — and its whole-app boot and DB connections — indefinitely (notably a retired-release worker draining after a deploy). For a **pooled** job the whole child running it is killed, so its concurrent in-flight siblings on that child are also reported `failed` and requeued — a hung JS job can't be cancelled any other way — before a replacement child is spawned. It's a coarse safety net, not per-job tuning: it applies to every forked and pooled job, so set it well above the longest legitimate job. Omit it, or set `null`/`<= 0`, to disable. `"inline"` jobs are not covered — they share the worker's process and can't be killed without killing the worker. See [docs/background-jobs.md](docs/background-jobs.md#job-timeout-hung-runner-backstop).
|
|
@@ -786,9 +786,9 @@ export default class BackgroundJobsMain {
|
|
|
786
786
|
options: message.options || {}
|
|
787
787
|
})
|
|
788
788
|
|
|
789
|
-
jsonSocket.send({type: "schedule-replaced", ...result})
|
|
790
789
|
this._notifyEnqueued()
|
|
791
790
|
await this._drain()
|
|
791
|
+
jsonSocket.send({type: "schedule-replaced", ...result})
|
|
792
792
|
} catch (error) {
|
|
793
793
|
this._handleClientMutationError({
|
|
794
794
|
context: {jobName: message.jobName, scheduleKey: message.scheduleKey, stage: "background-job-replace-scheduled"},
|
|
@@ -812,9 +812,9 @@ export default class BackgroundJobsMain {
|
|
|
812
812
|
try {
|
|
813
813
|
const result = await this.store.cancelScheduled(message.scheduleKey)
|
|
814
814
|
|
|
815
|
-
jsonSocket.send({type: "schedule-cancelled", ...result})
|
|
816
815
|
this._notifyEnqueued()
|
|
817
816
|
await this._drain()
|
|
817
|
+
jsonSocket.send({type: "schedule-cancelled", ...result})
|
|
818
818
|
} catch (error) {
|
|
819
819
|
this._handleClientMutationError({
|
|
820
820
|
context: {scheduleKey: message.scheduleKey, stage: "background-job-cancel-scheduled"},
|
|
@@ -1354,9 +1354,20 @@ export default class BackgroundJobsMain {
|
|
|
1354
1354
|
if (this.dispatchStrategy === "polling") return
|
|
1355
1355
|
|
|
1356
1356
|
const next = await this.store.nextScheduledJob()
|
|
1357
|
-
|
|
1357
|
+
let delay
|
|
1358
|
+
|
|
1359
|
+
if (next && typeof next.scheduledAtMs === "number") {
|
|
1360
|
+
delay = Math.max(0, Math.min(next.scheduledAtMs - Date.now(), MAX_TIMER_MS))
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// `nextScheduledJob` only returns future jobs, so a job that became
|
|
1364
|
+
// eligible after the drain's eligible-job probe is invisible to it. If one
|
|
1365
|
+
// is dispatchable now, arm a 0-delay re-drain so it is dispatched
|
|
1366
|
+
// immediately instead of being stranded until the next future timer (or
|
|
1367
|
+
// external signal) fires.
|
|
1368
|
+
if (await this.nextAvailableJobForReadyWorkers()) delay = 0
|
|
1358
1369
|
|
|
1359
|
-
|
|
1370
|
+
if (typeof delay !== "number") return
|
|
1360
1371
|
|
|
1361
1372
|
this._scheduledTimer = setTimeout(() => {
|
|
1362
1373
|
this._scheduledTimer = undefined
|
|
@@ -81,8 +81,8 @@ export default class BackgroundJobsScheduler {
|
|
|
81
81
|
* Narrows the runtime value to the documented type.
|
|
82
82
|
* @type {Array<ReturnType<typeof setTimeout>>} */
|
|
83
83
|
this.timeoutIds = []
|
|
84
|
-
/** @type {
|
|
85
|
-
this.
|
|
84
|
+
/** @type {Map<string, Promise<void>>} - In-flight scheduled enqueues by schedule key that shutdown must drain. */
|
|
85
|
+
this.pendingEnqueuesByJobKey = new Map()
|
|
86
86
|
/**
|
|
87
87
|
* Narrows the runtime value to the documented type.
|
|
88
88
|
* @type {boolean} - True between stop() and the next start(); cron self-rescheduler checks this so a stop() during an in-flight enqueue doesn't immediately re-arm.
|
|
@@ -131,7 +131,7 @@ export default class BackgroundJobsScheduler {
|
|
|
131
131
|
this.intervalIds = []
|
|
132
132
|
this.timeoutIds = []
|
|
133
133
|
|
|
134
|
-
await Promise.all(this.
|
|
134
|
+
await Promise.all(this.pendingEnqueuesByJobKey.values())
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
/**
|
|
@@ -181,13 +181,15 @@ export default class BackgroundJobsScheduler {
|
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
const timeoutId = setTimeout(() => {
|
|
184
|
-
|
|
184
|
+
const scheduledEnqueue = this.runScheduledJob({jobConfiguration, jobKey})
|
|
185
185
|
|
|
186
186
|
const intervalId = setInterval(() => {
|
|
187
|
-
|
|
187
|
+
return this.runScheduledJob({jobConfiguration, jobKey})
|
|
188
188
|
}, intervalMs)
|
|
189
189
|
|
|
190
190
|
this.intervalIds.push(intervalId)
|
|
191
|
+
|
|
192
|
+
return scheduledEnqueue
|
|
191
193
|
}, firstInMs)
|
|
192
194
|
|
|
193
195
|
this.timeoutIds.push(timeoutId)
|
|
@@ -242,13 +244,15 @@ export default class BackgroundJobsScheduler {
|
|
|
242
244
|
* @returns {Promise<void>} - Resolves after the enqueue attempt finishes.
|
|
243
245
|
*/
|
|
244
246
|
async runScheduledJob({jobConfiguration, jobKey}) {
|
|
247
|
+
if (this.stopped || this.pendingEnqueuesByJobKey.has(jobKey)) return
|
|
248
|
+
|
|
245
249
|
const pendingEnqueue = this.enqueueScheduledJob({jobConfiguration, jobKey})
|
|
246
|
-
this.
|
|
250
|
+
this.pendingEnqueuesByJobKey.set(jobKey, pendingEnqueue)
|
|
247
251
|
|
|
248
252
|
try {
|
|
249
253
|
await pendingEnqueue
|
|
250
254
|
} finally {
|
|
251
|
-
this.
|
|
255
|
+
this.pendingEnqueuesByJobKey.delete(jobKey)
|
|
252
256
|
}
|
|
253
257
|
}
|
|
254
258
|
|
|
@@ -77,7 +77,8 @@ const SORTABLE_COLUMNS = {
|
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
79
|
* Serializes concurrent `_applySchema` runs within THIS process, keyed by database
|
|
80
|
-
* identifier
|
|
80
|
+
* identifier, before callers without an existing connection check one out. Two
|
|
81
|
+
* stores that share one connection (SingleMultiUse / SQLite)
|
|
81
82
|
* otherwise interleave the multi-step table rebuild and corrupt it (the jobs table
|
|
82
83
|
* is left as its `*_velocious_rebuild` temp). A DB advisory lock can't fix that: on
|
|
83
84
|
* a session-scoped / re-entrant driver (MySQL `GET_LOCK`) a second acquire on the
|
|
@@ -1059,22 +1060,17 @@ export default class BackgroundJobsStore {
|
|
|
1059
1060
|
* @returns {Promise<void>} - Resolves when the schema is present.
|
|
1060
1061
|
*/
|
|
1061
1062
|
async _ensureSchema(existingDb) {
|
|
1062
|
-
|
|
1063
|
-
await this._applySchema(existingDb)
|
|
1064
|
-
|
|
1065
|
-
return
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
await this._withDb((db) => this._applySchema(db))
|
|
1063
|
+
await this._applySchema(existingDb)
|
|
1069
1064
|
}
|
|
1070
1065
|
|
|
1071
1066
|
/**
|
|
1072
|
-
*
|
|
1073
|
-
*
|
|
1074
|
-
* @param {import("../database/drivers/base.js").default}
|
|
1067
|
+
* Serializes creation or upgrade of the background-jobs schema, checking out a
|
|
1068
|
+
* connection only after earlier schema work has completed when one is not supplied.
|
|
1069
|
+
* @param {import("../database/drivers/base.js").default} [existingDb] - Caller-owned
|
|
1070
|
+
* database connection.
|
|
1075
1071
|
* @returns {Promise<void>} - Resolves when the schema is present.
|
|
1076
1072
|
*/
|
|
1077
|
-
async _applySchema(
|
|
1073
|
+
async _applySchema(existingDb) {
|
|
1078
1074
|
// Serialize concurrent schema applies within this process, keyed by database
|
|
1079
1075
|
// identifier (see `schemaApplyChains`). The per-step locks inside the steps use
|
|
1080
1076
|
// DIFFERENT lock names, so two concurrent callers could otherwise each hold a
|
|
@@ -1084,7 +1080,16 @@ export default class BackgroundJobsStore {
|
|
|
1084
1080
|
// the second caller then re-checks and finds every step already done.
|
|
1085
1081
|
const identifier = this.getDatabaseIdentifier() ?? "default"
|
|
1086
1082
|
const previous = schemaApplyChains.get(identifier) ?? Promise.resolve()
|
|
1087
|
-
const
|
|
1083
|
+
const applyWithConnection = async () => {
|
|
1084
|
+
if (existingDb) {
|
|
1085
|
+
await this._applySchemaSteps(existingDb)
|
|
1086
|
+
|
|
1087
|
+
return
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
await this._withDb((db) => this._applySchemaSteps(db))
|
|
1091
|
+
}
|
|
1092
|
+
const run = previous.then(applyWithConnection, applyWithConnection)
|
|
1088
1093
|
|
|
1089
1094
|
// Keep the chain alive regardless of this run's outcome so one failed apply does
|
|
1090
1095
|
// not wedge later callers; this run still propagates its own result/error.
|
|
@@ -312,6 +312,7 @@
|
|
|
312
312
|
* @property {string} controller - Controller class name.
|
|
313
313
|
* @property {string} [action] - Controller action or endpoint label.
|
|
314
314
|
* @property {"index" | "find" | "create" | "update" | "destroy" | "attach" | "attachmentList" | "download" | "url" | "custom-command"} [commandType] - Frontend-model command type.
|
|
315
|
+
* @property {string} [correlationId] - Server-generated identifier shared by an unexpected client error and framework reports.
|
|
315
316
|
* @property {boolean} [expectedError] - Whether the error is an expected user-flow failure.
|
|
316
317
|
* @property {boolean} [frontendModelEndpoint] - Whether the error came from the frontend-model endpoint.
|
|
317
318
|
* @property {string} [model] - Frontend-model name from the failed request.
|
package/build/configuration.js
CHANGED
|
@@ -286,6 +286,19 @@ export default class VelociousConfiguration {
|
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
this._isInitialized = false
|
|
289
|
+
this._modelsInitialized = false
|
|
290
|
+
/**
|
|
291
|
+
* Invalidates model phases that started before database connections closed.
|
|
292
|
+
* @type {number}
|
|
293
|
+
*/
|
|
294
|
+
this._modelInitializationGeneration = 0
|
|
295
|
+
/**
|
|
296
|
+
* In-progress `initializeModels()` promise. Model initialization is an
|
|
297
|
+
* atomic bootstrap phase: concurrent callers share it, and a rejection
|
|
298
|
+
* leaves the phase eligible for a later complete attempt.
|
|
299
|
+
* @type {Promise<void> | undefined}
|
|
300
|
+
*/
|
|
301
|
+
this._initializeModelsPromise = undefined
|
|
289
302
|
/**
|
|
290
303
|
* In-progress `initialize()` promise, memoized so concurrent callers await
|
|
291
304
|
* the same bootstrap. Reset to undefined if initialization fails.
|
|
@@ -2020,25 +2033,39 @@ export default class VelociousConfiguration {
|
|
|
2020
2033
|
* @returns {Promise<void>} - Resolves when complete.
|
|
2021
2034
|
*/
|
|
2022
2035
|
async initializeModels(args = {type: "server"}) {
|
|
2023
|
-
if (
|
|
2024
|
-
|
|
2036
|
+
if (this._modelsInitialized) return
|
|
2037
|
+
if (this._initializeModelsPromise) return await this._initializeModelsPromise
|
|
2025
2038
|
|
|
2039
|
+
const modelInitializationGeneration = this._modelInitializationGeneration
|
|
2040
|
+
const initializeModelsPromise = (async () => {
|
|
2026
2041
|
const shouldSkipDummyModelInitialization = process.env.VELOCIOUS_SKIP_DUMMY_MODEL_INITIALIZATION === "1"
|
|
2027
2042
|
&& process.env.VELOCIOUS_BROWSER_TESTS === "true"
|
|
2028
2043
|
&& this.getEnvironment() === "test"
|
|
2029
2044
|
|
|
2030
|
-
if (shouldSkipDummyModelInitialization) {
|
|
2031
|
-
|
|
2045
|
+
if (!shouldSkipDummyModelInitialization) {
|
|
2046
|
+
if (this._initializeModels) {
|
|
2047
|
+
await this._initializeModels({configuration: this, type: args.type})
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
await this.getEnvironmentHandler().initializePackageModels(this)
|
|
2051
|
+
await initializeAuditedModelRelationships(this)
|
|
2052
|
+
|
|
2053
|
+
await this.getEnvironmentHandler().initializeFrontendModelWebsocketPublishers(this)
|
|
2032
2054
|
}
|
|
2033
2055
|
|
|
2034
|
-
if (this.
|
|
2035
|
-
|
|
2056
|
+
if (this._modelInitializationGeneration === modelInitializationGeneration) {
|
|
2057
|
+
this._modelsInitialized = true
|
|
2036
2058
|
}
|
|
2059
|
+
})()
|
|
2037
2060
|
|
|
2038
|
-
|
|
2039
|
-
await initializeAuditedModelRelationships(this)
|
|
2061
|
+
this._initializeModelsPromise = initializeModelsPromise
|
|
2040
2062
|
|
|
2041
|
-
|
|
2063
|
+
try {
|
|
2064
|
+
await initializeModelsPromise
|
|
2065
|
+
} finally {
|
|
2066
|
+
if (this._initializeModelsPromise === initializeModelsPromise) {
|
|
2067
|
+
this._initializeModelsPromise = undefined
|
|
2068
|
+
}
|
|
2042
2069
|
}
|
|
2043
2070
|
}
|
|
2044
2071
|
|
|
@@ -2073,6 +2100,12 @@ export default class VelociousConfiguration {
|
|
|
2073
2100
|
|
|
2074
2101
|
this._initializePromise = (async () => {
|
|
2075
2102
|
await this.initializeModels({type})
|
|
2103
|
+
|
|
2104
|
+
// Model initialization can be invalidated by a concurrent connection close.
|
|
2105
|
+
// If models are not ready, stop without marking the configuration initialized
|
|
2106
|
+
// so the next caller retries a full bootstrap.
|
|
2107
|
+
if (!this._modelsInitialized) return
|
|
2108
|
+
|
|
2076
2109
|
await this.getEnvironmentHandler().autoDiscoverResources(this)
|
|
2077
2110
|
this._mergeDiscoveredAbilityResources()
|
|
2078
2111
|
this._validateResourceRelationshipsOnModels()
|
|
@@ -2104,6 +2137,13 @@ export default class VelociousConfiguration {
|
|
|
2104
2137
|
this._initializePromise = undefined
|
|
2105
2138
|
throw error
|
|
2106
2139
|
}
|
|
2140
|
+
|
|
2141
|
+
// If the inner IIFE returned without marking the configuration initialized
|
|
2142
|
+
// (e.g. because models were invalidated mid-bootstrap), clear the promise so
|
|
2143
|
+
// a later call retries a full bootstrap.
|
|
2144
|
+
if (!this._isInitialized) {
|
|
2145
|
+
this._initializePromise = undefined
|
|
2146
|
+
}
|
|
2107
2147
|
}
|
|
2108
2148
|
|
|
2109
2149
|
/**
|
|
@@ -3244,8 +3284,10 @@ export default class VelociousConfiguration {
|
|
|
3244
3284
|
PoolClass.clearGlobalConnections(this)
|
|
3245
3285
|
}
|
|
3246
3286
|
|
|
3247
|
-
// Allow
|
|
3287
|
+
// Allow full re-initialization after connections are closed.
|
|
3288
|
+
this._modelInitializationGeneration += 1
|
|
3248
3289
|
this._modelsInitialized = false
|
|
3290
|
+
this._isInitialized = false
|
|
3249
3291
|
}
|
|
3250
3292
|
})()
|
|
3251
3293
|
|
|
@@ -20,6 +20,9 @@ export default class VelociousDatabaseDriversMysqlStructureSql {
|
|
|
20
20
|
const {driver} = this
|
|
21
21
|
const isMariaDb = await this._isMariaDb()
|
|
22
22
|
const rows = await driver.query("SELECT table_name, table_type FROM information_schema.tables WHERE table_schema = DATABASE() ORDER BY table_type, table_name")
|
|
23
|
+
const foreignKeyRows = await driver.query("SELECT table_name, referenced_table_name FROM information_schema.key_column_usage WHERE table_schema = DATABASE() AND referenced_table_schema = DATABASE() AND referenced_table_name IS NOT NULL")
|
|
24
|
+
const baseTableNames = []
|
|
25
|
+
const views = []
|
|
23
26
|
const statements = []
|
|
24
27
|
|
|
25
28
|
for (const row of rows) {
|
|
@@ -31,24 +34,82 @@ export default class VelociousDatabaseDriversMysqlStructureSql {
|
|
|
31
34
|
if (!tableName || !tableType) continue
|
|
32
35
|
|
|
33
36
|
if (tableType == "BASE TABLE") {
|
|
34
|
-
|
|
35
|
-
const rawCreateStatement = this._mysqlCreateStatement(createRows?.[0])
|
|
36
|
-
const createStatement = rawCreateStatement ? this._stripAutoIncrement(rawCreateStatement) : null
|
|
37
|
-
|
|
38
|
-
if (createStatement) statements.push(normalizeSqlStatement(createStatement))
|
|
37
|
+
baseTableNames.push(tableName)
|
|
39
38
|
} else if (tableType == "VIEW" || (isMariaDb && tableType == "SYSTEM VIEW")) {
|
|
40
|
-
|
|
41
|
-
const createStatement = this._mysqlCreateStatement(createRows?.[0])
|
|
42
|
-
|
|
43
|
-
if (createStatement) statements.push(normalizeSqlStatement(createStatement))
|
|
39
|
+
views.push(tableName)
|
|
44
40
|
}
|
|
45
41
|
}
|
|
46
42
|
|
|
43
|
+
for (const tableName of this._orderBaseTables({foreignKeyRows, tableNames: baseTableNames})) {
|
|
44
|
+
const createRows = await driver.query(`SHOW CREATE TABLE ${driver.quoteTable(tableName)}`)
|
|
45
|
+
const rawCreateStatement = this._mysqlCreateStatement(createRows?.[0])
|
|
46
|
+
const createStatement = rawCreateStatement ? this._stripAutoIncrement(rawCreateStatement) : null
|
|
47
|
+
|
|
48
|
+
if (createStatement) statements.push(normalizeSqlStatement(createStatement))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const tableName of views) {
|
|
52
|
+
const createRows = await driver.query(`SHOW CREATE VIEW ${driver.quoteTable(tableName)}`)
|
|
53
|
+
const createStatement = this._mysqlCreateStatement(createRows?.[0])
|
|
54
|
+
|
|
55
|
+
if (createStatement) statements.push(normalizeSqlStatement(createStatement))
|
|
56
|
+
}
|
|
57
|
+
|
|
47
58
|
if (statements.length == 0) return null
|
|
48
59
|
|
|
49
60
|
return `${statements.join("\n\n")}\n`
|
|
50
61
|
}
|
|
51
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Orders tables so referenced tables are created before their dependents.
|
|
65
|
+
* @param {object} args - Options object.
|
|
66
|
+
* @param {Array<Record<string, ?>>} args.foreignKeyRows - Foreign key metadata rows.
|
|
67
|
+
* @param {string[]} args.tableNames - Base table names in their existing order.
|
|
68
|
+
* @returns {string[]} - Ordered table names.
|
|
69
|
+
*/
|
|
70
|
+
_orderBaseTables({foreignKeyRows, tableNames}) {
|
|
71
|
+
const pendingTableNames = new Set(tableNames)
|
|
72
|
+
/** @type {Record<string, Set<string>>} */
|
|
73
|
+
const dependenciesByTableName = {}
|
|
74
|
+
const orderedTableNames = []
|
|
75
|
+
|
|
76
|
+
for (const tableName of tableNames) {
|
|
77
|
+
dependenciesByTableName[tableName] = new Set()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const row of foreignKeyRows) {
|
|
81
|
+
const tableNameValue = row.table_name || row.TABLE_NAME
|
|
82
|
+
const referencedTableNameValue = row.referenced_table_name || row.REFERENCED_TABLE_NAME
|
|
83
|
+
const tableName = tableNameValue ? String(tableNameValue) : ""
|
|
84
|
+
const referencedTableName = referencedTableNameValue ? String(referencedTableNameValue) : ""
|
|
85
|
+
|
|
86
|
+
if (tableName == referencedTableName || !pendingTableNames.has(tableName) || !pendingTableNames.has(referencedTableName)) continue
|
|
87
|
+
|
|
88
|
+
dependenciesByTableName[tableName].add(referencedTableName)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
while (pendingTableNames.size > 0) {
|
|
92
|
+
const nextTableName = tableNames.find((tableName) => {
|
|
93
|
+
if (!pendingTableNames.has(tableName)) return false
|
|
94
|
+
|
|
95
|
+
return Array.from(dependenciesByTableName[tableName]).every((dependencyTableName) => !pendingTableNames.has(dependencyTableName))
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
if (!nextTableName) {
|
|
99
|
+
for (const tableName of tableNames) {
|
|
100
|
+
if (pendingTableNames.has(tableName)) orderedTableNames.push(tableName)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
break
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
orderedTableNames.push(nextTableName)
|
|
107
|
+
pendingTableNames.delete(nextTableName)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return orderedTableNames
|
|
111
|
+
}
|
|
112
|
+
|
|
52
113
|
/**
|
|
53
114
|
* Runs is maria db.
|
|
54
115
|
* @returns {Promise<boolean>} - Resolves with Whether maria db.
|
|
@@ -161,22 +161,22 @@ function queryForEntry({entry, modelClass, parentIds, sourceModel}) {
|
|
|
161
161
|
|
|
162
162
|
const foreignKey = relationship.getForeignKey()
|
|
163
163
|
/**
|
|
164
|
-
*
|
|
164
|
+
* Mandatory cohort conditions.
|
|
165
165
|
* @type {Record<string, ?>} */
|
|
166
|
-
const
|
|
166
|
+
const mandatoryWhereConditions = {[foreignKey]: parentIds}
|
|
167
167
|
|
|
168
168
|
if (relationship.getPolymorphic && relationship.getPolymorphic()) {
|
|
169
169
|
const typeColumn = relationship.getPolymorphicTypeColumn()
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (entry.where) {
|
|
174
|
-
Object.assign(whereConditions, entry.where)
|
|
170
|
+
mandatoryWhereConditions[typeColumn] = modelClass.getModelName()
|
|
175
171
|
}
|
|
176
172
|
|
|
177
173
|
const baseQuery = sourceModel.queryForModel(targetModelClass)
|
|
178
174
|
baseQuery._forceQualifyBaseTable = true
|
|
179
|
-
baseQuery.where(
|
|
175
|
+
baseQuery.where(mandatoryWhereConditions)
|
|
176
|
+
|
|
177
|
+
if (entry.where) {
|
|
178
|
+
baseQuery.where(entry.where)
|
|
179
|
+
}
|
|
180
180
|
|
|
181
181
|
const countQuery = relationship.applyScope(baseQuery)
|
|
182
182
|
|
|
@@ -6,9 +6,11 @@ import TestFilesFinder from "../../../../testing/test-files-finder.js"
|
|
|
6
6
|
import TestRunner from "../../../../testing/test-runner.js"
|
|
7
7
|
import TestSuiteSplitter from "../../../../testing/test-suite-splitter.js"
|
|
8
8
|
import {normalizeExamplePatterns, parseFilters} from "../../../../testing/test-filter-parser.js"
|
|
9
|
+
import {prepareSourcePeerPackage} from "../../source-peer-package.js"
|
|
9
10
|
|
|
10
11
|
export default class VelociousCliCommandsTest extends BaseCommand {
|
|
11
12
|
async execute() {
|
|
13
|
+
await prepareSourcePeerPackage()
|
|
12
14
|
this.getConfiguration().setEnvironment("test")
|
|
13
15
|
|
|
14
16
|
let directory
|