velocious 1.0.700 → 1.0.702
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 +1 -0
- package/build/configuration-types.js +73 -3
- package/build/configuration.js +113 -20
- package/build/frontend-model-resource/base-resource.js +150 -2
- package/build/frontend-models/resource-definition.js +47 -0
- package/build/src/configuration-types.d.ts +168 -7
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +68 -4
- package/build/src/configuration.d.ts +22 -2
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +110 -20
- package/build/src/frontend-model-resource/base-resource.d.ts +76 -3
- package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
- package/build/src/frontend-model-resource/base-resource.js +127 -3
- package/build/src/frontend-models/resource-definition.d.ts.map +1 -1
- package/build/src/frontend-models/resource-definition.js +43 -1
- package/build/src/sync/local-mutation-log.d.ts +6 -3
- package/build/src/sync/local-mutation-log.d.ts.map +1 -1
- package/build/src/sync/local-mutation-log.js +11 -4
- package/build/src/sync/peer-mutation-bundle.d.ts +26 -0
- package/build/src/sync/peer-mutation-bundle.d.ts.map +1 -1
- package/build/src/sync/peer-mutation-bundle.js +7 -1
- package/build/src/sync/sync-api-client.d.ts +3 -1
- package/build/src/sync/sync-api-client.d.ts.map +1 -1
- package/build/src/sync/sync-api-client.js +46 -8
- package/build/src/sync/sync-client-types.d.ts +24 -12
- package/build/src/sync/sync-client-types.d.ts.map +1 -1
- package/build/src/sync/sync-client-types.js +7 -5
- package/build/src/sync/sync-client.d.ts +101 -0
- package/build/src/sync/sync-client.d.ts.map +1 -1
- package/build/src/sync/sync-client.js +236 -24
- package/build/src/sync/sync-offline-session.d.ts +328 -0
- package/build/src/sync/sync-offline-session.d.ts.map +1 -0
- package/build/src/sync/sync-offline-session.js +617 -0
- package/build/src/sync/sync-resource-base.d.ts +26 -28
- package/build/src/sync/sync-resource-base.d.ts.map +1 -1
- package/build/src/sync/sync-resource-base.js +11 -45
- package/build/sync/local-mutation-log.js +10 -3
- package/build/sync/peer-mutation-bundle.js +6 -0
- package/build/sync/sync-api-client.js +53 -7
- package/build/sync/sync-client-types.js +6 -4
- package/build/sync/sync-client.js +264 -21
- package/build/sync/sync-offline-session.js +713 -0
- package/build/sync/sync-resource-base.js +10 -53
- package/package.json +1 -1
- package/src/configuration-types.js +73 -3
- package/src/configuration.js +113 -20
- package/src/frontend-model-resource/base-resource.js +150 -2
- package/src/frontend-models/resource-definition.js +47 -0
- package/src/sync/local-mutation-log.js +10 -3
- package/src/sync/peer-mutation-bundle.js +6 -0
- package/src/sync/sync-api-client.js +53 -7
- package/src/sync/sync-client-types.js +6 -4
- package/src/sync/sync-client.js +264 -21
- package/src/sync/sync-offline-session.js +713 -0
- package/src/sync/sync-resource-base.js +10 -53
|
@@ -6,7 +6,6 @@ import FrontendModelBaseResource from "../frontend-model-resource/base-resource.
|
|
|
6
6
|
import SyncEnvelopeReplayService from "./sync-envelope-replay-service.js"
|
|
7
7
|
import SyncModelChangeFeedService from "./sync-model-change-feed-service.js"
|
|
8
8
|
import {syncUpstreamImporterForConfiguration} from "./sync-upstream-importer.js"
|
|
9
|
-
import VelociousError from "../velocious-error.js"
|
|
10
9
|
|
|
11
10
|
/**
|
|
12
11
|
* Optional client-declared sync scope carried on a changes request.
|
|
@@ -26,8 +25,6 @@ import VelociousError from "../velocious-error.js"
|
|
|
26
25
|
* `resourceId`/`resourceType` keep working unchanged.
|
|
27
26
|
* @typedef {Record<string, ReturnType<typeof JSON.parse>> & {resourceId: string, resourceType: string}} ChangeDeliverableSyncEntry
|
|
28
27
|
*/
|
|
29
|
-
const QUICK_SEARCH_COLUMN = "quickSearch"
|
|
30
|
-
|
|
31
28
|
/**
|
|
32
29
|
* Base resource for Velocious sync endpoints.
|
|
33
30
|
*
|
|
@@ -49,61 +46,21 @@ export default class SyncResourceBase extends FrontendModelBaseResource {
|
|
|
49
46
|
static ReplayServiceClass = undefined
|
|
50
47
|
|
|
51
48
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* @
|
|
56
|
-
static quickSearchColumns = null
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Applies frontend-model index searches, expanding declared quick searches.
|
|
60
|
-
* @param {object} args - Search args.
|
|
61
|
-
* @param {import("../frontend-model-resource/base-resource.js").FrontendModelResourceController} args.controller - Controller handling the query.
|
|
62
|
-
* @param {import("../frontend-model-resource/base-resource.js").FrontendModelResourceAnyQuery} args.query - Query instance.
|
|
63
|
-
* @param {import("../frontend-model-resource/base-resource.js").FrontendModelResourceSearch} args.search - Search params.
|
|
64
|
-
* @returns {void}
|
|
49
|
+
* Keeps the sync resource's established public error contract for invalid
|
|
50
|
+
* quick-search payloads; the quick-search mechanism itself lives on
|
|
51
|
+
* FrontendModelBaseResource.
|
|
52
|
+
* @returns {string} - Error label.
|
|
65
53
|
*/
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
super.applyFrontendModelIndexSearch({controller, query, search})
|
|
54
|
+
quickSearchErrorLabel() {
|
|
55
|
+
return "Sync quick search"
|
|
70
56
|
}
|
|
71
57
|
|
|
72
58
|
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* @param {object} args - Search args.
|
|
76
|
-
* @param {import("../frontend-model-resource/base-resource.js").FrontendModelResourceAnyQuery} args.query - Query to filter.
|
|
77
|
-
* @param {import("../frontend-model-resource/base-resource.js").FrontendModelResourceSearch} args.search - Search payload.
|
|
78
|
-
* @returns {boolean} Whether the search was handled as a quick search.
|
|
59
|
+
* Keeps the established public error code for invalid quick-search payloads.
|
|
60
|
+
* @returns {string} - Error code.
|
|
79
61
|
*/
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (!quickSearchColumns || quickSearchColumns.length === 0) return false
|
|
84
|
-
if (search.path.length > 0 || search.column !== QUICK_SEARCH_COLUMN) return false
|
|
85
|
-
|
|
86
|
-
if (search.operator !== "like") {
|
|
87
|
-
throw VelociousError.safe("Sync quick search must use the like operator.", {code: "sync-invalid-quick-search"})
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (typeof search.value !== "string") {
|
|
91
|
-
throw VelociousError.safe("Sync quick search must be a string.", {code: "sync-invalid-quick-search"})
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const trimmedValue = search.value.trim()
|
|
95
|
-
|
|
96
|
-
if (!trimmedValue) return true
|
|
97
|
-
|
|
98
|
-
const tableSql = query.driver.quoteTable(query.getTableReferenceForJoin())
|
|
99
|
-
const likeValue = `%${trimmedValue}%`
|
|
100
|
-
const conditions = quickSearchColumns.map((columnName) => (
|
|
101
|
-
`${tableSql}.${query.driver.quoteColumn(columnName)} LIKE ${query.driver.quote(likeValue)}`
|
|
102
|
-
))
|
|
103
|
-
|
|
104
|
-
query.where(`(${conditions.join(" OR ")})`)
|
|
105
|
-
|
|
106
|
-
return true
|
|
62
|
+
quickSearchErrorCode() {
|
|
63
|
+
return "sync-invalid-quick-search"
|
|
107
64
|
}
|
|
108
65
|
|
|
109
66
|
// The declarative `static writableAttributes` permit list lives on
|
package/package.json
CHANGED
|
@@ -329,7 +329,7 @@
|
|
|
329
329
|
* @property {string} [host] - Hostname to bind the HTTP server to.
|
|
330
330
|
* @property {boolean} [inProcess] - Run HTTP handlers in the main thread instead of worker threads.
|
|
331
331
|
* @property {number} [maxBufferedResponseBodyBytes] - Maximum UTF-8/binary byte length accepted by buffered response bodies. Unbounded when absent; streamed file responses are unaffected.
|
|
332
|
-
* @property {number} [maxRequestBodyBytes] - Maximum decoded request-body bytes accepted before routing. Unbounded when absent.
|
|
332
|
+
* @property {number} [maxRequestBodyBytes] - Maximum decoded request-body bytes accepted before routing. Unbounded when absent, except the built-in MCP endpoint, which defaults to a 1 MiB cap that this option overrides.
|
|
333
333
|
* @property {HttpRequestBodyPolicyResolver} [requestBodyPolicyResolver] - Optional synchronous header-stage resolver for per-request limits and raw-body handling.
|
|
334
334
|
* @property {number} [maxWorkers] - Backward-compatible alias for workers.
|
|
335
335
|
* @property {number} [port] - Port to bind the HTTP server to.
|
|
@@ -476,9 +476,19 @@
|
|
|
476
476
|
* @property {"hasOne" | "hasMany"} type - Attachment cardinality.
|
|
477
477
|
*/
|
|
478
478
|
|
|
479
|
+
/**
|
|
480
|
+
* One declarative quick-search column. A plain string is a root-table column;
|
|
481
|
+
* `{path, column}` searches a column on a joined relationship path.
|
|
482
|
+
* @typedef {object} FrontendModelQuickSearchColumn
|
|
483
|
+
* @property {string[]} [path] - Relationship path from the root model to the column's table.
|
|
484
|
+
* @property {string} column - Database column name to search.
|
|
485
|
+
*/
|
|
486
|
+
|
|
479
487
|
/**
|
|
480
488
|
* @typedef {object} FrontendModelResourceConfiguration
|
|
481
489
|
* @property {Array<string | FrontendModelAttributeConfiguration> | Record<string, FrontendModelAttributeConfiguration | import("./database/drivers/base-column.js").default | boolean>} attributes - Attributes to expose on the frontend model.
|
|
490
|
+
* @property {Array<FrontendModelQuickSearchColumn | string>} [quickSearchColumns] - Declarative quick-search text columns; an index search on the pseudo-column `quickSearch` expands to an OR of LIKE conditions over these columns.
|
|
491
|
+
* @property {Array<FrontendModelQuickSearchColumn | string>} [quickSearchNumericColumns] - Declarative quick-search numeric columns; an integer quick-search value also matches these columns exactly.
|
|
482
492
|
* @property {string[]} [abilities] - Additional camelCase ability action names to expose for per-record `record.can(action)` reads. Base CRUD actions (`read`, `create`, `update`, `destroy`) are always included and must not be listed here.
|
|
483
493
|
* @property {Record<string, FrontendModelAttachmentConfiguration>} [attachments] - Attachment helpers keyed by attachment name.
|
|
484
494
|
* @property {string[]} [commands] - Legacy built-in command names (`index`, `find`, `create`, `update`, `destroy`, `attach`, `download`, `url`).
|
|
@@ -588,17 +598,75 @@
|
|
|
588
598
|
* @property {() => boolean | Promise<boolean>} [isOnline] - Connectivity gate for pulls and replays. Defaults to always online.
|
|
589
599
|
* @property {string} [mountPath] - Mount path the server serves the sync endpoints under (match the server's `sync.api.mountPath`). Defaults to "/velocious/sync"; normalization strips trailing slashes and always fills in the default.
|
|
590
600
|
* @property {(error: Error) => void} [onError] - Reports background replay/pull failures. Defaults to rethrowing.
|
|
601
|
+
* @property {VelociousSyncClientOfflineConfiguration} [offline] - Long-offline session configuration consumed by `startOfflineSession()`: device identity, enrollment, grant scopes, signed-replay path, and the backend public key for peer import.
|
|
591
602
|
* @property {VelociousSyncClientRealtimeConfiguration} [realtime] - Realtime push configuration consumed by `subscribeRealtime(...)`.
|
|
592
603
|
* @property {VelociousSyncClientTransport} transport - Transport posting to the framework sync endpoints (e.g. the frontend-model websocket client).
|
|
593
604
|
* @property {VelociousSyncRealtimeWebsocketClient} [websocketClient] - Shared app-lifetime websocket client (the low-level form) that all sync traffic rides. Provide the same instance the frontend-model transport uses so one connection carries everything. When set, the realtime bridge subscribes channels on it and never owns its lifecycle: unsubscribing closes only channel subscriptions, leaving the socket connected.
|
|
594
605
|
* @property {string | (() => string | null | undefined)} [websocketUrl] - Shared app-lifetime websocket URL. When set (and no `websocketClient` is given), the framework builds and owns one reconnecting `VelociousWebsocketClient` for all sync traffic, connected on first use.
|
|
595
606
|
*/
|
|
596
607
|
|
|
608
|
+
/**
|
|
609
|
+
* App-owned durable storage for the device signing identity (the backend-signed
|
|
610
|
+
* device certificate plus the device private key). Native apps back this with
|
|
611
|
+
* secure storage; the framework never embeds a storage technology.
|
|
612
|
+
* @typedef {object} VelociousSyncClientDeviceIdentityStorage
|
|
613
|
+
* @property {() => import("./sync/sync-offline-session.js").SyncOfflineDeviceIdentity | null | undefined | Promise<import("./sync/sync-offline-session.js").SyncOfflineDeviceIdentity | null | undefined>} load - Reads the stored identity (null when the device has none yet).
|
|
614
|
+
* @property {(identity: import("./sync/sync-offline-session.js").SyncOfflineDeviceIdentity) => Promise<void> | void} save - Persists the identity.
|
|
615
|
+
*/
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* App-owned durable storage for retained offline grants. Pending mutations must
|
|
619
|
+
* always replay with the grant they were signed under, so a device that
|
|
620
|
+
* restarts while offline needs its issued grants back.
|
|
621
|
+
* @typedef {object} VelociousSyncClientGrantStorage
|
|
622
|
+
* @property {() => Array<import("./sync/offline-grant.js").SignedOfflineGrant> | Promise<Array<import("./sync/offline-grant.js").SignedOfflineGrant>>} load - Reads the retained grants.
|
|
623
|
+
* @property {(grants: Array<import("./sync/offline-grant.js").SignedOfflineGrant>) => Promise<void> | void} save - Persists the retained grants.
|
|
624
|
+
*/
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Client-side long-offline session configuration consumed by
|
|
628
|
+
* `SyncClient.startOfflineSession()`. The framework owns the session lifecycle
|
|
629
|
+
* (identity, grant bootstrap/refresh, signing, signed replay, peer bundles) over
|
|
630
|
+
* the built-in `/frontend-models/sync/bootstrap` and
|
|
631
|
+
* `/frontend-models/sync/replay` endpoints; the app only owns durable storage,
|
|
632
|
+
* the enrollment hook, and the grant scopes.
|
|
633
|
+
* @typedef {object} VelociousSyncClientOfflineConfiguration
|
|
634
|
+
* @property {VelociousSyncClientDeviceIdentityStorage} deviceIdentityStorage - App-owned durable device-identity storage adapter.
|
|
635
|
+
* @property {(args: {configuration: import("./configuration.js").default, publicKey: import("./sync/device-identity.js").SyncJsonWebKey}) => import("./sync/device-identity.js").DeviceCertificate | Promise<import("./sync/device-identity.js").DeviceCertificate>} enrollDevice - App-owned enrollment hook: asks the backend to register the device public key and returns the backend-signed certificate.
|
|
636
|
+
* @property {(args: {configuration: import("./configuration.js").default}) => Record<string, import("./configuration-types.js").FrontendModelSyncJsonValue> | Promise<Record<string, import("./configuration-types.js").FrontendModelSyncJsonValue>>} grantScopes - Resolves the scopes the bootstrap request asks the backend to materialize into the grant.
|
|
637
|
+
* @property {string} [bootstrapPath] - Signed-bootstrap endpoint path. Defaults to "/frontend-models/sync/bootstrap".
|
|
638
|
+
* @property {string} [replayPath] - Signed-replay endpoint path. Defaults to "/frontend-models/sync/replay".
|
|
639
|
+
* @property {import("./sync/device-identity.js").SyncJsonWebKey} [backendPublicKey] - Backend public key used to verify device certificates and imported peer mutations.
|
|
640
|
+
* @property {VelociousSyncClientGrantStorage} [grantStorage] - Optional durable grant retention adapter.
|
|
641
|
+
* @property {import("./sync/local-mutation-log.js").default} [mutationLog] - Durable local mutation log the offline session replays and exports. Defaults to the conflict-tracking log declared by the synced models.
|
|
642
|
+
* @property {() => Date} [now] - Session clock. Defaults to the current time.
|
|
643
|
+
*/
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Normalized client-side long-offline session configuration. Same shape as
|
|
647
|
+
* {@link VelociousSyncClientOfflineConfiguration} with the defaults filled in.
|
|
648
|
+
* @typedef {VelociousSyncClientOfflineConfiguration & {
|
|
649
|
+
* bootstrapPath: string,
|
|
650
|
+
* configuration: import("./configuration.js").default,
|
|
651
|
+
* now: () => Date,
|
|
652
|
+
* replayPath: string
|
|
653
|
+
* }} NormalizedVelociousSyncClientOfflineConfiguration
|
|
654
|
+
*/
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Normalized client-side sync configuration. Same shape as
|
|
658
|
+
* {@link VelociousSyncClientConfiguration} with the defaults filled in.
|
|
659
|
+
* @typedef {VelociousSyncClientConfiguration & {
|
|
660
|
+
* mountPath: string,
|
|
661
|
+
* offline: NormalizedVelociousSyncClientOfflineConfiguration | undefined
|
|
662
|
+
* }} NormalizedVelociousSyncClientConfiguration
|
|
663
|
+
*/
|
|
664
|
+
|
|
597
665
|
/**
|
|
598
666
|
* Velocious sync configuration.
|
|
599
667
|
* @typedef {object} VelociousSyncConfiguration
|
|
600
668
|
* @property {VelociousSyncApiConfiguration} [api] - Auto-mounts the Velocious sync changes/replay endpoints for this resource class.
|
|
601
|
-
* @property {
|
|
669
|
+
* @property {NormalizedVelociousSyncClientConfiguration | undefined} client - Normalized client-side sync configuration consumed by `SyncClient.fromConfiguration(...)`.
|
|
602
670
|
* @property {import("./sync/device-identity.js").SyncJsonWebKey | null} [deviceCertificateBackendPublicKey] - Public backend key used to verify offline device certificates for sync replay.
|
|
603
671
|
* @property {number} [changeFeedRetentionSize] - Number of accepted server changes retained before clients must refresh from snapshot.
|
|
604
672
|
* @property {Array<import("./sync/offline-grant.js").OfflineGrantSigningKey>} offlineGrantSigningKeys - Signing keys used to issue and verify offline grants. Secrets must never be exposed to clients.
|
|
@@ -617,13 +685,15 @@
|
|
|
617
685
|
*/
|
|
618
686
|
|
|
619
687
|
/**
|
|
620
|
-
* @typedef {Omit<FrontendModelResourceConfiguration, "abilities" | "builtInCollectionCommands" | "builtInMemberCommands" | "collectionCommands" | "commands" | "memberCommands" | "sync"> & {
|
|
688
|
+
* @typedef {Omit<FrontendModelResourceConfiguration, "abilities" | "builtInCollectionCommands" | "builtInMemberCommands" | "collectionCommands" | "commands" | "memberCommands" | "quickSearchColumns" | "quickSearchNumericColumns" | "sync"> & {
|
|
621
689
|
* abilities: FrontendModelResourceAbilitiesConfiguration
|
|
622
690
|
* builtInCollectionCommands: Record<string, string>
|
|
623
691
|
* builtInMemberCommands: Record<string, string>
|
|
624
692
|
* collectionCommands: Record<string, string>
|
|
625
693
|
* commandMetadata: Record<string, {args: Array<{name: string, type: string}>, returnType: string | null}>
|
|
626
694
|
* memberCommands: Record<string, string>
|
|
695
|
+
* quickSearchColumns: FrontendModelQuickSearchColumn[] | null
|
|
696
|
+
* quickSearchNumericColumns: FrontendModelQuickSearchColumn[] | null
|
|
627
697
|
* sync?: NormalizedFrontendModelResourceSyncConfiguration
|
|
628
698
|
* }} NormalizedFrontendModelResourceConfiguration
|
|
629
699
|
*/
|
package/src/configuration.js
CHANGED
|
@@ -229,6 +229,14 @@ function normalizeHttpCompression(value) {
|
|
|
229
229
|
}
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Default request-body limit for the built-in MCP endpoint. JSON-RPC messages are small,
|
|
234
|
+
* so the framework caps them instead of making every app that enables `mcp: true` restate
|
|
235
|
+
* a request-body policy. Explicit `httpServer.maxRequestBodyBytes` or a resolver limit wins.
|
|
236
|
+
* @type {number}
|
|
237
|
+
*/
|
|
238
|
+
const DEFAULT_MCP_MAX_REQUEST_BODY_BYTES = 1024 * 1024
|
|
239
|
+
|
|
232
240
|
export default class VelociousConfiguration {
|
|
233
241
|
/**
|
|
234
242
|
* Close database connections promise.
|
|
@@ -776,31 +784,58 @@ export default class VelociousConfiguration {
|
|
|
776
784
|
resolveHttpRequestBodyPolicy(args) {
|
|
777
785
|
const resolver = this.httpServer.requestBodyPolicyResolver
|
|
778
786
|
const configuredPolicy = resolver ? resolver(args) : undefined
|
|
787
|
+
/** @type {import("./configuration-types.js").ResolvedHttpRequestBodyPolicy} */
|
|
788
|
+
let resolvedPolicy
|
|
779
789
|
|
|
780
790
|
if (configuredPolicy === undefined) {
|
|
781
|
-
|
|
782
|
-
}
|
|
791
|
+
resolvedPolicy = {maxRequestBodyBytes: this.httpServer.maxRequestBodyBytes, mode: "parsed"}
|
|
792
|
+
} else {
|
|
793
|
+
if (!configuredPolicy || typeof configuredPolicy !== "object" || Array.isArray(configuredPolicy)) {
|
|
794
|
+
throw new TypeError("httpServer.requestBodyPolicyResolver must return an object or undefined")
|
|
795
|
+
}
|
|
783
796
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
}
|
|
797
|
+
const {maxRequestBodyBytes, mode = "parsed", ...restPolicy} = configuredPolicy
|
|
798
|
+
const unknownKeys = Object.keys(restPolicy)
|
|
787
799
|
|
|
788
|
-
|
|
789
|
-
|
|
800
|
+
if (unknownKeys.length > 0) {
|
|
801
|
+
throw new TypeError(`httpServer.requestBodyPolicyResolver returned unknown keys: ${unknownKeys.join(", ")} (supported: maxRequestBodyBytes, mode)`)
|
|
802
|
+
}
|
|
803
|
+
if (mode !== "parsed" && mode !== "raw") {
|
|
804
|
+
throw new TypeError(`httpServer.requestBodyPolicyResolver mode must be "parsed" or "raw", got: ${String(mode)}`)
|
|
805
|
+
}
|
|
790
806
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
807
|
+
resolvedPolicy = {
|
|
808
|
+
maxRequestBodyBytes: maxRequestBodyBytes === undefined
|
|
809
|
+
? this.httpServer.maxRequestBodyBytes
|
|
810
|
+
: optionalPositiveSafeInteger(maxRequestBodyBytes, "httpServer.requestBodyPolicyResolver maxRequestBodyBytes"),
|
|
811
|
+
mode
|
|
812
|
+
}
|
|
796
813
|
}
|
|
797
814
|
|
|
798
|
-
return {
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
815
|
+
return this._applyMcpRequestBodyPolicyDefaults({args, policy: resolvedPolicy})
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Enforces the built-in MCP endpoint's body contract without per-app plumbing: the
|
|
820
|
+
* JSON-RPC envelope is read from parsed params, so raw mode is rejected with an
|
|
821
|
+
* actionable error, and a framework default byte cap applies when the app configured none.
|
|
822
|
+
* @param {{args: import("./configuration-types.js").HttpRequestBodyPolicyResolverArgs, policy: import("./configuration-types.js").ResolvedHttpRequestBodyPolicy}} options - Resolution inputs.
|
|
823
|
+
* @returns {import("./configuration-types.js").ResolvedHttpRequestBodyPolicy} - Effective request policy.
|
|
824
|
+
*/
|
|
825
|
+
_applyMcpRequestBodyPolicyDefaults({args, policy}) {
|
|
826
|
+
if (!this._mcp.enabled) return policy
|
|
827
|
+
if (args.httpMethod !== "POST") return policy
|
|
828
|
+
if (args.path.split("?")[0] !== this._mcp.path) return policy
|
|
829
|
+
|
|
830
|
+
if (policy.mode === "raw") {
|
|
831
|
+
throw new TypeError(
|
|
832
|
+
`The built-in MCP endpoint at ${this._mcp.path} requires parsed request bodies because its JSON-RPC envelope is read from params. Remove the raw-mode httpServer.requestBodyPolicyResolver entry for this path.`
|
|
833
|
+
)
|
|
803
834
|
}
|
|
835
|
+
|
|
836
|
+
if (policy.maxRequestBodyBytes !== undefined) return policy
|
|
837
|
+
|
|
838
|
+
return {maxRequestBodyBytes: DEFAULT_MCP_MAX_REQUEST_BODY_BYTES, mode: policy.mode}
|
|
804
839
|
}
|
|
805
840
|
|
|
806
841
|
/**
|
|
@@ -865,7 +900,7 @@ export default class VelociousConfiguration {
|
|
|
865
900
|
/**
|
|
866
901
|
* Normalizes client-side sync configuration consumed by `SyncClient.fromConfiguration(...)`.
|
|
867
902
|
* @param {import("./configuration-types.js").VelociousSyncClientConfiguration | undefined} client - Client-side sync configuration.
|
|
868
|
-
* @returns {import("./configuration-types.js").
|
|
903
|
+
* @returns {import("./configuration-types.js").NormalizedVelociousSyncClientConfiguration | undefined} - Normalized client-side sync configuration.
|
|
869
904
|
*/
|
|
870
905
|
_normalizeSyncClientConfiguration(client) {
|
|
871
906
|
if (client === undefined || client === null) return undefined
|
|
@@ -874,11 +909,11 @@ export default class VelociousConfiguration {
|
|
|
874
909
|
throw new Error("sync.client must be an object with transport and authenticationToken")
|
|
875
910
|
}
|
|
876
911
|
|
|
877
|
-
const {authenticationToken, batchSize, isOnline, mountPath, onError, realtime, transport, websocketClient, websocketUrl, ...restClient} = client
|
|
912
|
+
const {authenticationToken, batchSize, isOnline, mountPath, onError, offline, realtime, transport, websocketClient, websocketUrl, ...restClient} = client
|
|
878
913
|
const restClientKeys = Object.keys(restClient)
|
|
879
914
|
|
|
880
915
|
if (restClientKeys.length > 0) {
|
|
881
|
-
throw new Error(`sync.client received unknown keys: ${restClientKeys.join(", ")} (supported: authenticationToken, batchSize, isOnline, mountPath, onError, realtime, transport, websocketClient, websocketUrl)`)
|
|
916
|
+
throw new Error(`sync.client received unknown keys: ${restClientKeys.join(", ")} (supported: authenticationToken, batchSize, isOnline, mountPath, onError, offline, realtime, transport, websocketClient, websocketUrl)`)
|
|
882
917
|
}
|
|
883
918
|
if (!transport || typeof transport !== "object" || typeof transport.post !== "function") {
|
|
884
919
|
throw new Error("sync.client.transport must be an object with a post(path, body) method (like the frontend-model websocket client)")
|
|
@@ -910,6 +945,7 @@ export default class VelociousConfiguration {
|
|
|
910
945
|
batchSize,
|
|
911
946
|
isOnline,
|
|
912
947
|
mountPath: (mountPath || "/velocious/sync").replace(/\/+$/u, "") || "/",
|
|
948
|
+
offline: this._normalizeSyncClientOfflineConfiguration(offline),
|
|
913
949
|
onError,
|
|
914
950
|
realtime,
|
|
915
951
|
transport,
|
|
@@ -918,6 +954,63 @@ export default class VelociousConfiguration {
|
|
|
918
954
|
}
|
|
919
955
|
}
|
|
920
956
|
|
|
957
|
+
/**
|
|
958
|
+
* Normalizes the client-side long-offline session configuration consumed by
|
|
959
|
+
* `SyncClient.startOfflineSession()`. The framework owns the session
|
|
960
|
+
* lifecycle; the app provides durable identity storage, the enrollment hook,
|
|
961
|
+
* and the grant scopes callback.
|
|
962
|
+
* @param {import("./configuration-types.js").VelociousSyncClientOfflineConfiguration | undefined} offline - Offline configuration block.
|
|
963
|
+
* @returns {import("./configuration-types.js").NormalizedVelociousSyncClientOfflineConfiguration | undefined} - Normalized offline configuration.
|
|
964
|
+
*/
|
|
965
|
+
_normalizeSyncClientOfflineConfiguration(offline) {
|
|
966
|
+
if (offline === undefined || offline === null) return undefined
|
|
967
|
+
|
|
968
|
+
if (typeof offline !== "object" || Array.isArray(offline)) {
|
|
969
|
+
throw new Error("sync.client.offline must be an object with deviceIdentityStorage, enrollDevice, and grantScopes")
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const {backendPublicKey, bootstrapPath, deviceIdentityStorage, enrollDevice, grantScopes, grantStorage, mutationLog, now, replayPath, ...restOffline} = offline
|
|
973
|
+
const restOfflineKeys = Object.keys(restOffline)
|
|
974
|
+
|
|
975
|
+
if (restOfflineKeys.length > 0) {
|
|
976
|
+
throw new Error(`sync.client.offline received unknown keys: ${restOfflineKeys.join(", ")} (supported: backendPublicKey, bootstrapPath, deviceIdentityStorage, enrollDevice, grantScopes, grantStorage, mutationLog, now, replayPath)`)
|
|
977
|
+
}
|
|
978
|
+
if (!deviceIdentityStorage || typeof deviceIdentityStorage !== "object" || Array.isArray(deviceIdentityStorage) || typeof deviceIdentityStorage.load !== "function" || typeof deviceIdentityStorage.save !== "function") {
|
|
979
|
+
throw new Error("sync.client.offline.deviceIdentityStorage must be an object with load() and save(identity) methods")
|
|
980
|
+
}
|
|
981
|
+
if (typeof enrollDevice !== "function") throw new Error("sync.client.offline.enrollDevice must be a function resolving the backend-signed device certificate")
|
|
982
|
+
if (typeof grantScopes !== "function") throw new Error("sync.client.offline.grantScopes must be a function resolving the bootstrap grant scopes")
|
|
983
|
+
if (backendPublicKey !== undefined && (typeof backendPublicKey !== "object" || backendPublicKey === null || Array.isArray(backendPublicKey))) {
|
|
984
|
+
throw new Error("sync.client.offline.backendPublicKey must be a public JSON Web Key object")
|
|
985
|
+
}
|
|
986
|
+
if (bootstrapPath !== undefined && (typeof bootstrapPath !== "string" || !bootstrapPath.startsWith("/"))) {
|
|
987
|
+
throw new Error(`sync.client.offline.bootstrapPath must start with '/', got: ${String(bootstrapPath)}`)
|
|
988
|
+
}
|
|
989
|
+
if (replayPath !== undefined && (typeof replayPath !== "string" || !replayPath.startsWith("/"))) {
|
|
990
|
+
throw new Error(`sync.client.offline.replayPath must start with '/', got: ${String(replayPath)}`)
|
|
991
|
+
}
|
|
992
|
+
if (grantStorage !== undefined && (typeof grantStorage !== "object" || grantStorage === null || Array.isArray(grantStorage) || typeof grantStorage.load !== "function" || typeof grantStorage.save !== "function")) {
|
|
993
|
+
throw new Error("sync.client.offline.grantStorage must be an object with load() and save(grants) methods")
|
|
994
|
+
}
|
|
995
|
+
if (mutationLog !== undefined && (typeof mutationLog !== "object" || mutationLog === null || typeof mutationLog.append !== "function")) {
|
|
996
|
+
throw new Error("sync.client.offline.mutationLog must be a LocalMutationLog")
|
|
997
|
+
}
|
|
998
|
+
if (now !== undefined && typeof now !== "function") throw new Error("sync.client.offline.now must be a function resolving the session clock")
|
|
999
|
+
|
|
1000
|
+
return {
|
|
1001
|
+
backendPublicKey,
|
|
1002
|
+
bootstrapPath: (bootstrapPath || "/frontend-models/sync/bootstrap").replace(/\/+$/u, "") || "/",
|
|
1003
|
+
configuration: this,
|
|
1004
|
+
deviceIdentityStorage,
|
|
1005
|
+
enrollDevice,
|
|
1006
|
+
grantScopes,
|
|
1007
|
+
grantStorage,
|
|
1008
|
+
mutationLog,
|
|
1009
|
+
now: now || (() => new Date()),
|
|
1010
|
+
replayPath: (replayPath || "/frontend-models/sync/replay").replace(/\/+$/u, "") || "/"
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
921
1014
|
/**
|
|
922
1015
|
* Normalizes sync API endpoint configuration.
|
|
923
1016
|
* @param {import("./configuration-types.js").VelociousSyncApiConfiguration | undefined} api - Sync API configuration.
|
|
@@ -29,6 +29,7 @@ import VelociousError from "../velocious-error.js"
|
|
|
29
29
|
* applyFrontendModelPagination: (args: {pagination: FrontendModelResourcePagination, query: import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>}) => void,
|
|
30
30
|
* applyFrontendModelSearch: (args: {query: import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>, search: FrontendModelResourceSearch}) => void,
|
|
31
31
|
* applyFrontendModelSort: (args: {query: import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>, sort: FrontendModelResourceSort}) => void,
|
|
32
|
+
* ensureFrontendModelJoinPath: (args: {path: string[], query: import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>}) => void,
|
|
32
33
|
* frontendModelAbilityAction: (action: FrontendModelResourceAction) => string,
|
|
33
34
|
* frontendModelAbilityAuthorizedQuery: (action: FrontendModelResourceAction, options?: FrontendModelResourceAuthorizedQueryOptions) => import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>,
|
|
34
35
|
* frontendModelAuthorizedQuery: (action: FrontendModelResourceAction, options?: FrontendModelResourceAuthorizedQueryOptions) => import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>,
|
|
@@ -195,6 +196,9 @@ export function frontendModelResourceInternalConstructor(ResourceClass) {
|
|
|
195
196
|
return /** @type {FrontendModelResourceInternalConstructor<typeof import("../database/record/index.js").default, typeof import("../database/record/index.js").default>} */ (/** @type {unknown} */ (ResourceClass))
|
|
196
197
|
}
|
|
197
198
|
|
|
199
|
+
const QUICK_SEARCH_COLUMN = "quickSearch"
|
|
200
|
+
|
|
201
|
+
|
|
198
202
|
/**
|
|
199
203
|
* Base class for backend frontend-model resources.
|
|
200
204
|
* @template {FrontendModelResourceModelClass} [TModelClass=typeof import("../database/record/index.js").default]
|
|
@@ -235,6 +239,23 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
235
239
|
/** @type {ReturnType<typeof JSON.parse>} */
|
|
236
240
|
static SharedResource = undefined
|
|
237
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Declarative quick-search text columns. When declared, an index search on
|
|
244
|
+
* the pseudo-column `quickSearch` expands to an OR of LIKE conditions over
|
|
245
|
+
* these columns instead of hitting the controller default. Each entry is a
|
|
246
|
+
* root-table column name or a `{path, column}` pair for a joined
|
|
247
|
+
* relationship path.
|
|
248
|
+
* @type {Array<import("../configuration-types.js").FrontendModelQuickSearchColumn | string> | null | undefined} */
|
|
249
|
+
static quickSearchColumns = undefined
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Declarative quick-search numeric columns. When the trimmed quick-search
|
|
253
|
+
* value is a safe non-negative integer, each declared column also gets an
|
|
254
|
+
* exact `=` condition ORed into the quick-search predicate. Same entry shape
|
|
255
|
+
* as {@link FrontendModelBaseResource.quickSearchColumns}.
|
|
256
|
+
* @type {Array<import("../configuration-types.js").FrontendModelQuickSearchColumn | string> | null | undefined} */
|
|
257
|
+
static quickSearchNumericColumns = undefined
|
|
258
|
+
|
|
238
259
|
/**
|
|
239
260
|
* Declarative writable-attribute permit list (camelCase attribute names)
|
|
240
261
|
* used as the default {@link FrontendModelBaseResource#permittedParams} and
|
|
@@ -282,7 +303,7 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
282
303
|
/**
|
|
283
304
|
* Reads a static resource config value from the environment resource first,
|
|
284
305
|
* then from the shared resource.
|
|
285
|
-
* @param {"abilities" | "attachments" | "attributes" | "builtInCollectionCommands" | "builtInMemberCommands" | "collectionCommands" | "commands" | "memberCommands" | "modelName" | "primaryKey" | "relationships" | "server" | "sync" | "translatedAttributes" | "writableAttributes"} name - Static config property name.
|
|
306
|
+
* @param {"abilities" | "attachments" | "attributes" | "builtInCollectionCommands" | "builtInMemberCommands" | "collectionCommands" | "commands" | "memberCommands" | "modelName" | "primaryKey" | "quickSearchColumns" | "quickSearchNumericColumns" | "relationships" | "server" | "sync" | "translatedAttributes" | "writableAttributes"} name - Static config property name.
|
|
286
307
|
* @returns {ReturnType<typeof JSON.parse>} - Resolved config value.
|
|
287
308
|
*/
|
|
288
309
|
static sharedResourceStaticValue(name) {
|
|
@@ -459,6 +480,8 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
459
480
|
const memberCommands = this.sharedResourceStaticValue("memberCommands")
|
|
460
481
|
const modelName = this.sharedResourceStaticValue("modelName")
|
|
461
482
|
const primaryKey = this.sharedResourceStaticValue("primaryKey")
|
|
483
|
+
const quickSearchColumns = this.sharedResourceStaticValue("quickSearchColumns")
|
|
484
|
+
const quickSearchNumericColumns = this.sharedResourceStaticValue("quickSearchNumericColumns")
|
|
462
485
|
const relationships = this.sharedResourceStaticValue("relationships")
|
|
463
486
|
const server = this.sharedResourceStaticValue("server")
|
|
464
487
|
const sync = this.sharedResourceStaticValue("sync")
|
|
@@ -476,6 +499,8 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
476
499
|
if (memberCommands) config.memberCommands = /** @type {string[]} */ (memberCommands)
|
|
477
500
|
if (modelName) config.modelName = /** @type {string} */ (modelName)
|
|
478
501
|
if (primaryKey) config.primaryKey = /** @type {string | string[]} */ (primaryKey)
|
|
502
|
+
if (quickSearchColumns !== undefined) config.quickSearchColumns = /** @type {Array<import("../configuration-types.js").FrontendModelQuickSearchColumn | string>} */ (quickSearchColumns)
|
|
503
|
+
if (quickSearchNumericColumns !== undefined) config.quickSearchNumericColumns = /** @type {Array<import("../configuration-types.js").FrontendModelQuickSearchColumn | string>} */ (quickSearchNumericColumns)
|
|
479
504
|
if (relationships) config.relationships = /** @type {string[]} */ (relationships)
|
|
480
505
|
if (server) config.server = /** @type {import("../configuration-types.js").FrontendModelResourceServerConfiguration} */ (server)
|
|
481
506
|
if (sync !== undefined) config.sync = /** @type {import("../configuration-types.js").FrontendModelResourceSyncConfiguration | boolean} */ (sync)
|
|
@@ -949,7 +974,8 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
949
974
|
}
|
|
950
975
|
|
|
951
976
|
/**
|
|
952
|
-
* Applies frontend-model index search
|
|
977
|
+
* Applies frontend-model index searches, expanding a declared quick search
|
|
978
|
+
* before falling back to the controller default.
|
|
953
979
|
* @param {object} args - Search args.
|
|
954
980
|
* @param {FrontendModelResourceController} args.controller - Controller handling the query.
|
|
955
981
|
* @param {FrontendModelResourceAnyQuery} args.query - Query instance.
|
|
@@ -957,9 +983,118 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
957
983
|
* @returns {void}
|
|
958
984
|
*/
|
|
959
985
|
applyFrontendModelIndexSearch({controller, query, search}) {
|
|
986
|
+
if (this.applyQuickSearch({controller, query, search})) return
|
|
987
|
+
|
|
960
988
|
controller.applyFrontendModelSearch({query, search})
|
|
961
989
|
}
|
|
962
990
|
|
|
991
|
+
/**
|
|
992
|
+
* Expands a `quickSearch` pseudo-column search into an OR of LIKE conditions
|
|
993
|
+
* over the declared {@link FrontendModelBaseResource.quickSearchColumns}, plus
|
|
994
|
+
* exact `=` conditions over {@link FrontendModelBaseResource.quickSearchNumericColumns}
|
|
995
|
+
* when the trimmed value is a safe non-negative integer.
|
|
996
|
+
* @param {object} args - Search args.
|
|
997
|
+
* @param {FrontendModelResourceController} args.controller - Controller handling the query.
|
|
998
|
+
* @param {FrontendModelResourceAnyQuery} args.query - Query to filter.
|
|
999
|
+
* @param {FrontendModelResourceSearch} args.search - Search payload.
|
|
1000
|
+
* @returns {boolean} - Whether the search was handled as a quick search.
|
|
1001
|
+
*/
|
|
1002
|
+
applyQuickSearch({controller, query, search}) {
|
|
1003
|
+
const quickSearchColumns = this.quickSearchColumnDefinitions()
|
|
1004
|
+
|
|
1005
|
+
if (!quickSearchColumns || quickSearchColumns.length === 0) return false
|
|
1006
|
+
if (search.path.length > 0 || search.column !== QUICK_SEARCH_COLUMN) return false
|
|
1007
|
+
|
|
1008
|
+
if (search.operator !== "like") {
|
|
1009
|
+
throw VelociousError.safe(`${this.quickSearchErrorLabel()} must use the like operator.`, {code: this.quickSearchErrorCode()})
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
if (typeof search.value !== "string") {
|
|
1013
|
+
throw VelociousError.safe(`${this.quickSearchErrorLabel()} must be a string.`, {code: this.quickSearchErrorCode()})
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
const trimmedValue = search.value.trim()
|
|
1017
|
+
|
|
1018
|
+
if (!trimmedValue) return true
|
|
1019
|
+
|
|
1020
|
+
const conditions = quickSearchColumns.map((column) => (
|
|
1021
|
+
`${this.quickSearchColumnSql({column, controller, query})} LIKE ${query.driver.quote(`%${trimmedValue}%`)}`
|
|
1022
|
+
))
|
|
1023
|
+
const numericSearchValue = quickSearchNumericValue(trimmedValue)
|
|
1024
|
+
|
|
1025
|
+
if (numericSearchValue !== null) {
|
|
1026
|
+
const quickSearchNumericColumns = this.quickSearchNumericColumnDefinitions() || []
|
|
1027
|
+
|
|
1028
|
+
for (const column of quickSearchNumericColumns) {
|
|
1029
|
+
conditions.push(`${this.quickSearchColumnSql({column, controller, query})} = ${query.driver.quote(numericSearchValue)}`)
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
query.where(`(${conditions.join(" OR ")})`)
|
|
1034
|
+
|
|
1035
|
+
return true
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* Resolves the declared quick-search text columns through the shared resource.
|
|
1040
|
+
* @returns {Array<import("../configuration-types.js").FrontendModelQuickSearchColumn | string> | null | undefined} - Declared columns.
|
|
1041
|
+
*/
|
|
1042
|
+
quickSearchColumnDefinitions() {
|
|
1043
|
+
const ResourceClass = /** @type {typeof FrontendModelBaseResource} */ (this.constructor)
|
|
1044
|
+
|
|
1045
|
+
return /** @type {Array<import("../configuration-types.js").FrontendModelQuickSearchColumn | string> | null | undefined} */ (ResourceClass.sharedResourceStaticValue("quickSearchColumns"))
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Resolves the declared quick-search numeric columns through the shared resource.
|
|
1050
|
+
* @returns {Array<import("../configuration-types.js").FrontendModelQuickSearchColumn | string> | null | undefined} - Declared columns.
|
|
1051
|
+
*/
|
|
1052
|
+
quickSearchNumericColumnDefinitions() {
|
|
1053
|
+
const ResourceClass = /** @type {typeof FrontendModelBaseResource} */ (this.constructor)
|
|
1054
|
+
|
|
1055
|
+
return /** @type {Array<import("../configuration-types.js").FrontendModelQuickSearchColumn | string> | null | undefined} */ (ResourceClass.sharedResourceStaticValue("quickSearchNumericColumns"))
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* Builds the quoted SQL reference for one quick-search column, adding the
|
|
1060
|
+
* relationship join when the column sits on a joined path.
|
|
1061
|
+
* @param {object} args - Column args.
|
|
1062
|
+
* @param {import("../configuration-types.js").FrontendModelQuickSearchColumn | string} args.column - Column declaration.
|
|
1063
|
+
* @param {FrontendModelResourceController} args.controller - Controller handling the query.
|
|
1064
|
+
* @param {FrontendModelResourceAnyQuery} args.query - Query being filtered.
|
|
1065
|
+
* @returns {string} - Quoted SQL column reference.
|
|
1066
|
+
*/
|
|
1067
|
+
quickSearchColumnSql({column, controller, query}) {
|
|
1068
|
+
const columnConfig = typeof column === "string" ? {column} : column
|
|
1069
|
+
const path = columnConfig.path || []
|
|
1070
|
+
|
|
1071
|
+
if (path.length > 0) {
|
|
1072
|
+
controller.ensureFrontendModelJoinPath({path, query})
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const tableReference = query.getTableReferenceForJoin(...path)
|
|
1076
|
+
|
|
1077
|
+
return `${query.driver.quoteTable(tableReference)}.${query.driver.quoteColumn(columnConfig.column)}`
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* Human-readable label used in invalid quick-search payload errors. Owners
|
|
1082
|
+
* with an established public error contract may override this.
|
|
1083
|
+
* @returns {string} - Error label.
|
|
1084
|
+
*/
|
|
1085
|
+
quickSearchErrorLabel() {
|
|
1086
|
+
return "Quick search"
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
/**
|
|
1090
|
+
* Error code used for invalid quick-search payload errors. Owners with an
|
|
1091
|
+
* established public error contract may override this.
|
|
1092
|
+
* @returns {string} - Error code.
|
|
1093
|
+
*/
|
|
1094
|
+
quickSearchErrorCode() {
|
|
1095
|
+
return "invalid-quick-search"
|
|
1096
|
+
}
|
|
1097
|
+
|
|
963
1098
|
/**
|
|
964
1099
|
* Applies frontend-model index sort.
|
|
965
1100
|
* @param {object} args - Sort args.
|
|
@@ -2125,3 +2260,16 @@ function filterWritableFrontendModelAttributes(
|
|
|
2125
2260
|
|
|
2126
2261
|
return writableAttributes
|
|
2127
2262
|
}
|
|
2263
|
+
|
|
2264
|
+
/**
|
|
2265
|
+
* Parses a trimmed quick-search value as a safe non-negative integer.
|
|
2266
|
+
* @param {string} searchValue - Trimmed quick-search value.
|
|
2267
|
+
* @returns {number | null} - Numeric search value, or null when not numeric.
|
|
2268
|
+
*/
|
|
2269
|
+
function quickSearchNumericValue(searchValue) {
|
|
2270
|
+
if (!/^[0-9]+$/u.test(searchValue)) return null
|
|
2271
|
+
|
|
2272
|
+
const numericValue = Number.parseInt(searchValue, 10)
|
|
2273
|
+
|
|
2274
|
+
return Number.isSafeInteger(numericValue) ? numericValue : null
|
|
2275
|
+
}
|