velocious 1.0.474 → 1.0.476
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 +10 -2
- package/build/configuration-types.js +39 -1
- package/build/configuration.js +46 -1
- package/build/database/drivers/base.js +67 -3
- package/build/database/drivers/mssql/index.js +13 -0
- package/build/database/drivers/mssql/sql/remove-index.js +6 -0
- package/build/database/drivers/mysql/index.js +87 -7
- package/build/database/drivers/mysql/sql/remove-index.js +6 -0
- package/build/database/drivers/pgsql/index.js +13 -0
- package/build/database/drivers/pgsql/sql/remove-index.js +6 -0
- package/build/database/drivers/sqlite/base.js +13 -0
- package/build/database/drivers/sqlite/connection-sql-js.js +16 -5
- package/build/database/drivers/sqlite/index.web.js +29 -15
- package/build/database/drivers/sqlite/sql/remove-index.js +6 -0
- package/build/database/drivers/sqlite/web-persistence.js +521 -0
- package/build/database/migration/index.js +44 -0
- package/build/database/pool/base-methods-forward.js +1 -0
- package/build/database/query/remove-index-base.js +39 -0
- package/build/database/record/index.js +25 -9
- package/build/environment-handlers/node/cli/commands/generate/base-models.js +36 -0
- package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +63 -0
- package/build/frontend-model-controller.js +401 -4
- package/build/frontend-model-resource/base-resource.js +5 -1
- package/build/frontend-models/base.js +146 -2
- package/build/frontend-models/query.js +3 -3
- package/build/frontend-models/resource-definition.js +304 -2
- package/build/routes/hooks/frontend-model-command-route-hook.js +16 -0
- package/build/src/configuration-types.d.ts +118 -2
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +36 -2
- package/build/src/configuration.d.ts +18 -1
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +42 -2
- package/build/src/database/drivers/base.d.ts +50 -0
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +62 -4
- package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mssql/index.js +12 -1
- package/build/src/database/drivers/mssql/sql/remove-index.d.ts +4 -0
- package/build/src/database/drivers/mssql/sql/remove-index.d.ts.map +1 -0
- package/build/src/database/drivers/mssql/sql/remove-index.js +5 -0
- package/build/src/database/drivers/mysql/index.d.ts +32 -3
- package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mysql/index.js +76 -7
- package/build/src/database/drivers/mysql/sql/remove-index.d.ts +4 -0
- package/build/src/database/drivers/mysql/sql/remove-index.d.ts.map +1 -0
- package/build/src/database/drivers/mysql/sql/remove-index.js +5 -0
- package/build/src/database/drivers/pgsql/index.d.ts.map +1 -1
- package/build/src/database/drivers/pgsql/index.js +12 -1
- package/build/src/database/drivers/pgsql/sql/remove-index.d.ts +4 -0
- package/build/src/database/drivers/pgsql/sql/remove-index.d.ts.map +1 -0
- package/build/src/database/drivers/pgsql/sql/remove-index.js +5 -0
- package/build/src/database/drivers/sqlite/base.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/base.js +12 -1
- package/build/src/database/drivers/sqlite/connection-sql-js.d.ts +8 -1
- package/build/src/database/drivers/sqlite/connection-sql-js.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/connection-sql-js.js +15 -6
- package/build/src/database/drivers/sqlite/index.web.d.ts +5 -5
- package/build/src/database/drivers/sqlite/index.web.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/index.web.js +27 -13
- package/build/src/database/drivers/sqlite/sql/remove-index.d.ts +4 -0
- package/build/src/database/drivers/sqlite/sql/remove-index.d.ts.map +1 -0
- package/build/src/database/drivers/sqlite/sql/remove-index.js +5 -0
- package/build/src/database/drivers/sqlite/web-persistence.d.ts +77 -0
- package/build/src/database/drivers/sqlite/web-persistence.d.ts.map +1 -0
- package/build/src/database/drivers/sqlite/web-persistence.js +464 -0
- package/build/src/database/migration/index.d.ts +25 -0
- package/build/src/database/migration/index.d.ts.map +1 -1
- package/build/src/database/migration/index.js +39 -1
- package/build/src/database/pool/base-methods-forward.d.ts.map +1 -1
- package/build/src/database/pool/base-methods-forward.js +2 -1
- package/build/src/database/query/remove-index-base.d.ts +35 -0
- package/build/src/database/query/remove-index-base.d.ts.map +1 -0
- package/build/src/database/query/remove-index-base.js +34 -0
- package/build/src/database/record/index.d.ts +11 -0
- package/build/src/database/record/index.d.ts.map +1 -1
- package/build/src/database/record/index.js +26 -11
- package/build/src/environment-handlers/node/cli/commands/generate/base-models.d.ts +15 -0
- package/build/src/environment-handlers/node/cli/commands/generate/base-models.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/generate/base-models.js +33 -1
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts +30 -0
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +53 -1
- package/build/src/frontend-model-controller.d.ts +96 -0
- package/build/src/frontend-model-controller.d.ts.map +1 -1
- package/build/src/frontend-model-controller.js +359 -5
- package/build/src/frontend-model-resource/base-resource.d.ts +4 -2
- package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
- package/build/src/frontend-model-resource/base-resource.js +7 -2
- package/build/src/frontend-models/base.d.ts +29 -0
- package/build/src/frontend-models/base.d.ts.map +1 -1
- package/build/src/frontend-models/base.js +131 -3
- package/build/src/frontend-models/query.js +4 -4
- package/build/src/frontend-models/resource-definition.d.ts +6 -0
- package/build/src/frontend-models/resource-definition.d.ts.map +1 -1
- package/build/src/frontend-models/resource-definition.js +272 -3
- package/build/src/routes/hooks/frontend-model-command-route-hook.d.ts.map +1 -1
- package/build/src/routes/hooks/frontend-model-command-route-hook.js +15 -1
- package/build/src/sync/device-identity.d.ts +240 -0
- package/build/src/sync/device-identity.d.ts.map +1 -0
- package/build/src/sync/device-identity.js +454 -0
- package/build/src/sync/local-mutation-log.d.ts +212 -0
- package/build/src/sync/local-mutation-log.d.ts.map +1 -0
- package/build/src/sync/local-mutation-log.js +404 -0
- package/build/src/sync/offline-grant.d.ts +157 -0
- package/build/src/sync/offline-grant.d.ts.map +1 -0
- package/build/src/sync/offline-grant.js +292 -0
- package/build/sync/device-identity.js +503 -0
- package/build/sync/local-mutation-log.js +448 -0
- package/build/sync/offline-grant.js +327 -0
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/configuration-types.js +39 -1
- package/src/configuration.js +46 -1
- package/src/database/drivers/base.js +67 -3
- package/src/database/drivers/mssql/index.js +13 -0
- package/src/database/drivers/mssql/sql/remove-index.js +6 -0
- package/src/database/drivers/mysql/index.js +87 -7
- package/src/database/drivers/mysql/sql/remove-index.js +6 -0
- package/src/database/drivers/pgsql/index.js +13 -0
- package/src/database/drivers/pgsql/sql/remove-index.js +6 -0
- package/src/database/drivers/sqlite/base.js +13 -0
- package/src/database/drivers/sqlite/connection-sql-js.js +16 -5
- package/src/database/drivers/sqlite/index.web.js +29 -15
- package/src/database/drivers/sqlite/sql/remove-index.js +6 -0
- package/src/database/drivers/sqlite/web-persistence.js +521 -0
- package/src/database/migration/index.js +44 -0
- package/src/database/pool/base-methods-forward.js +1 -0
- package/src/database/query/remove-index-base.js +39 -0
- package/src/database/record/index.js +25 -9
- package/src/environment-handlers/node/cli/commands/generate/base-models.js +36 -0
- package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +63 -0
- package/src/frontend-model-controller.js +401 -4
- package/src/frontend-model-resource/base-resource.js +5 -1
- package/src/frontend-models/base.js +146 -2
- package/src/frontend-models/query.js +3 -3
- package/src/frontend-models/resource-definition.js +304 -2
- package/src/routes/hooks/frontend-model-command-route-hook.js +16 -0
- package/src/sync/device-identity.js +503 -0
- package/src/sync/local-mutation-log.js +448 -0
- package/src/sync/offline-grant.js +327 -0
|
@@ -165,6 +165,8 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
165
165
|
static primaryKey = undefined
|
|
166
166
|
/** @type {import("../configuration-types.js").FrontendModelResourceServerConfiguration | undefined} */
|
|
167
167
|
static server = undefined
|
|
168
|
+
/** @type {import("../configuration-types.js").FrontendModelResourceSyncConfiguration | boolean | undefined} */
|
|
169
|
+
static sync = undefined
|
|
168
170
|
/** @type {string[] | undefined} */
|
|
169
171
|
static translatedAttributes = undefined
|
|
170
172
|
/** @type {?} */
|
|
@@ -204,7 +206,7 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
204
206
|
/**
|
|
205
207
|
* Reads a static resource config value from the environment resource first,
|
|
206
208
|
* then from the shared resource.
|
|
207
|
-
* @param {"abilities" | "attachments" | "attributes" | "builtInCollectionCommands" | "builtInMemberCommands" | "collectionCommands" | "commands" | "memberCommands" | "modelName" | "primaryKey" | "relationships" | "server" | "translatedAttributes"} name - Static config property name.
|
|
209
|
+
* @param {"abilities" | "attachments" | "attributes" | "builtInCollectionCommands" | "builtInMemberCommands" | "collectionCommands" | "commands" | "memberCommands" | "modelName" | "primaryKey" | "relationships" | "server" | "sync" | "translatedAttributes"} name - Static config property name.
|
|
208
210
|
* @returns {?} - Resolved config value.
|
|
209
211
|
*/
|
|
210
212
|
static sharedResourceStaticValue(name) {
|
|
@@ -359,6 +361,7 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
359
361
|
const primaryKey = this.sharedResourceStaticValue("primaryKey")
|
|
360
362
|
const relationships = this.sharedResourceStaticValue("relationships")
|
|
361
363
|
const server = this.sharedResourceStaticValue("server")
|
|
364
|
+
const sync = this.sharedResourceStaticValue("sync")
|
|
362
365
|
/** @type {import("../configuration-types.js").FrontendModelResourceConfiguration} */
|
|
363
366
|
const config = {
|
|
364
367
|
attributes: /** @type {Record<string, ?> | string[]} */ (attributes || [])
|
|
@@ -375,6 +378,7 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
375
378
|
if (primaryKey) config.primaryKey = /** @type {string} */ (primaryKey)
|
|
376
379
|
if (relationships) config.relationships = /** @type {string[]} */ (relationships)
|
|
377
380
|
if (server) config.server = /** @type {import("../configuration-types.js").FrontendModelResourceServerConfiguration} */ (server)
|
|
381
|
+
if (sync !== undefined) config.sync = /** @type {import("../configuration-types.js").FrontendModelResourceSyncConfiguration | boolean} */ (sync)
|
|
378
382
|
|
|
379
383
|
return config
|
|
380
384
|
}
|
|
@@ -58,7 +58,15 @@ import {readPayloadAssociationCount, readPayloadComputedAbility, readPayloadQuer
|
|
|
58
58
|
*/
|
|
59
59
|
/**
|
|
60
60
|
* Defines this typedef.
|
|
61
|
-
* @typedef {
|
|
61
|
+
* @typedef {Record<string, FrontendModelTransportValue>} FrontendModelSyncMetadata
|
|
62
|
+
*/
|
|
63
|
+
/**
|
|
64
|
+
* Defines this typedef.
|
|
65
|
+
* @typedef {{enabled: boolean, operations: string[], policyHash: string, policyVersion: string | null, metadata?: FrontendModelSyncMetadata}} FrontendModelSyncConfig
|
|
66
|
+
*/
|
|
67
|
+
/**
|
|
68
|
+
* Defines this typedef.
|
|
69
|
+
* @typedef {{attributes?: Array<string | FrontendModelAttributeDefinition> | Record<string, FrontendModelAttributeDefinition>, builtInCollectionCommands?: string[], builtInMemberCommands?: string[], collectionCommands?: string[], commands?: string[], memberCommands?: string[], attachments?: Record<string, FrontendModelAttachmentDefinition>, modelName?: string, nestedAttributes?: Record<string, {allowDestroy?: boolean, limit?: number}>, primaryKey?: string, relationships?: string[], sync?: FrontendModelSyncConfig}} FrontendModelResourceConfig
|
|
62
70
|
*/
|
|
63
71
|
/**
|
|
64
72
|
* Frontend model constructor type.
|
|
@@ -104,6 +112,7 @@ import {readPayloadAssociationCount, readPayloadComputedAbility, readPayloadQuer
|
|
|
104
112
|
* @property {Record<string, string> | (() => Record<string, string>)} [requestHeaders] - Extra HTTP/WS headers to attach to every frontend-model API request. Pass a function to compute them at request time (for example to include the current locale).
|
|
105
113
|
* @property {{get: () => string | null | undefined | Promise<string | null | undefined>, set: (sessionId: string) => void | Promise<void>, clear: () => void | Promise<void>}} [sessionStore] - Optional sessionId persistence hook forwarded to the internal `VelociousWebsocketClient` so WS sessions can be resumed across page reloads / app restarts.
|
|
106
114
|
* @property {string | (() => string | null | undefined)} [timeZone] - IANA timezone sent with every frontend-model API request for timezone-less datetime parsing.
|
|
115
|
+
* @property {{actorDeviceId: string, actorUserId: string, clientMutationId?: () => string, enabled?: boolean, mutationLog: import("../sync/local-mutation-log.js").default, now?: () => Date, offlineGrant: {id: string}}} [offlineSync] - Offline mutation queue configuration.
|
|
107
116
|
*/
|
|
108
117
|
/**
|
|
109
118
|
* FrontendModelIdleWaitArgs type.
|
|
@@ -777,6 +786,89 @@ function frontendModelClassFor(model) {
|
|
|
777
786
|
return /** @type {FrontendModelClass} */ (constructorValue)
|
|
778
787
|
}
|
|
779
788
|
|
|
789
|
+
/**
|
|
790
|
+
* Whether the configured offline queue should handle a model operation.
|
|
791
|
+
* @param {FrontendModelClass} ModelClass - Model class.
|
|
792
|
+
* @param {"create" | "update" | "destroy"} operation - Sync operation.
|
|
793
|
+
* @returns {boolean} - Whether to queue locally.
|
|
794
|
+
*/
|
|
795
|
+
function shouldQueueFrontendModelOperationOffline(ModelClass, operation) {
|
|
796
|
+
const offlineSync = frontendModelTransportConfig.offlineSync
|
|
797
|
+
|
|
798
|
+
if (!offlineSync?.enabled) return false
|
|
799
|
+
|
|
800
|
+
const syncConfig = ModelClass.resourceConfig().sync
|
|
801
|
+
|
|
802
|
+
if (!syncConfig?.enabled) return false
|
|
803
|
+
if (!syncConfig.operations.includes(operation)) throw new Error(`Offline sync for ${ModelClass.getModelName()} does not allow ${operation}`)
|
|
804
|
+
|
|
805
|
+
return true
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Queues an offline sync mutation.
|
|
810
|
+
* @param {object} args - Arguments.
|
|
811
|
+
* @param {Record<string, FrontendModelAttributeValue>} args.attributes - Mutation attributes.
|
|
812
|
+
* @param {string} [args.clientMutationId] - Pre-generated mutation id.
|
|
813
|
+
* @param {FrontendModelClass} args.ModelClass - Model class.
|
|
814
|
+
* @param {"create" | "update" | "destroy"} args.operation - Sync operation.
|
|
815
|
+
* @returns {Promise<string>} - Client mutation id.
|
|
816
|
+
*/
|
|
817
|
+
async function queueFrontendModelMutationOffline({attributes, clientMutationId: providedClientMutationId, ModelClass, operation}) {
|
|
818
|
+
const offlineSync = frontendModelTransportConfig.offlineSync
|
|
819
|
+
|
|
820
|
+
if (!offlineSync) throw new Error("Offline sync is not configured")
|
|
821
|
+
|
|
822
|
+
const syncConfig = ModelClass.resourceConfig().sync
|
|
823
|
+
if (!syncConfig?.enabled) throw new Error(`Offline sync is not enabled for ${ModelClass.getModelName()}`)
|
|
824
|
+
|
|
825
|
+
const now = offlineSync.now ? offlineSync.now() : new Date()
|
|
826
|
+
if (!(now instanceof Date) || Number.isNaN(now.getTime())) throw new Error("offlineSync.now must return a valid Date")
|
|
827
|
+
|
|
828
|
+
const clientMutationId = providedClientMutationId || (offlineSync.clientMutationId ? offlineSync.clientMutationId() : frontendModelOfflineMutationId())
|
|
829
|
+
if (typeof clientMutationId !== "string" || clientMutationId.length < 1) throw new Error("offlineSync.clientMutationId must return a non-empty string")
|
|
830
|
+
|
|
831
|
+
await offlineSync.mutationLog.append({
|
|
832
|
+
mutation: {
|
|
833
|
+
actorDeviceId: offlineSync.actorDeviceId,
|
|
834
|
+
actorUserId: offlineSync.actorUserId,
|
|
835
|
+
attributes: frontendModelSyncJsonObject(attributes),
|
|
836
|
+
baseVersion: null,
|
|
837
|
+
clientMutationId,
|
|
838
|
+
model: ModelClass.getModelName(),
|
|
839
|
+
occurredAt: now.toISOString(),
|
|
840
|
+
offlineGrantId: offlineSync.offlineGrant.id,
|
|
841
|
+
operation,
|
|
842
|
+
policyHash: syncConfig.policyHash
|
|
843
|
+
}
|
|
844
|
+
})
|
|
845
|
+
|
|
846
|
+
return clientMutationId
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Generates a frontend-model offline mutation id.
|
|
851
|
+
* @returns {string} - Local mutation id.
|
|
852
|
+
*/
|
|
853
|
+
function frontendModelOfflineMutationId() {
|
|
854
|
+
if (globalThis.crypto && typeof globalThis.crypto.randomUUID === "function") return globalThis.crypto.randomUUID()
|
|
855
|
+
|
|
856
|
+
return `frontend-mutation-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* Converts model attributes to sync-safe JSON payload values.
|
|
861
|
+
* @param {Record<string, FrontendModelAttributeValue>} attributes - Frontend model attributes.
|
|
862
|
+
* @returns {Record<string, import("../configuration-types.js").FrontendModelSyncJsonValue>} - Sync-safe attributes.
|
|
863
|
+
*/
|
|
864
|
+
function frontendModelSyncJsonObject(attributes) {
|
|
865
|
+
const serialized = JSON.parse(JSON.stringify(attributes))
|
|
866
|
+
|
|
867
|
+
if (!serialized || typeof serialized !== "object" || Array.isArray(serialized)) throw new Error("Expected sync mutation attributes object")
|
|
868
|
+
|
|
869
|
+
return /** @type {Record<string, import("../configuration-types.js").FrontendModelSyncJsonValue>} */ (serialized)
|
|
870
|
+
}
|
|
871
|
+
|
|
780
872
|
/**
|
|
781
873
|
* Runs normalize frontend attachment input.
|
|
782
874
|
* @param {?} input - Attachment input.
|
|
@@ -2779,6 +2871,10 @@ export default class FrontendModelBase {
|
|
|
2779
2871
|
// Reset cached internal client so the new sessionStore is picked up.
|
|
2780
2872
|
internalWebsocketClient = null
|
|
2781
2873
|
}
|
|
2874
|
+
|
|
2875
|
+
if (Object.prototype.hasOwnProperty.call(config, "offlineSync")) {
|
|
2876
|
+
frontendModelTransportConfig.offlineSync = config.offlineSync
|
|
2877
|
+
}
|
|
2782
2878
|
}
|
|
2783
2879
|
|
|
2784
2880
|
/**
|
|
@@ -3892,6 +3988,43 @@ export default class FrontendModelBase {
|
|
|
3892
3988
|
payload.attachments = attachments
|
|
3893
3989
|
}
|
|
3894
3990
|
|
|
3991
|
+
if (shouldQueueFrontendModelOperationOffline(ModelClass, commandType)) {
|
|
3992
|
+
const offlineAttributes = {...payload.attributes}
|
|
3993
|
+
let clientMutationId
|
|
3994
|
+
|
|
3995
|
+
if (isNew) {
|
|
3996
|
+
const primaryKey = ModelClass.primaryKey()
|
|
3997
|
+
const currentPrimaryKey = this.readAttribute(primaryKey)
|
|
3998
|
+
|
|
3999
|
+
if (currentPrimaryKey === undefined || currentPrimaryKey === null) {
|
|
4000
|
+
clientMutationId = frontendModelTransportConfig.offlineSync?.clientMutationId
|
|
4001
|
+
? frontendModelTransportConfig.offlineSync.clientMutationId()
|
|
4002
|
+
: frontendModelOfflineMutationId()
|
|
4003
|
+
this.setAttribute(primaryKey, clientMutationId)
|
|
4004
|
+
offlineAttributes[primaryKey] = clientMutationId
|
|
4005
|
+
}
|
|
4006
|
+
} else {
|
|
4007
|
+
offlineAttributes[ModelClass.primaryKey()] = payload.id
|
|
4008
|
+
}
|
|
4009
|
+
|
|
4010
|
+
if (payload.nestedAttributes !== undefined || payload.attachments !== undefined) {
|
|
4011
|
+
throw new Error(`Offline sync for ${ModelClass.name} does not support nested attributes or attachments yet`)
|
|
4012
|
+
}
|
|
4013
|
+
|
|
4014
|
+
await queueFrontendModelMutationOffline({
|
|
4015
|
+
attributes: offlineAttributes,
|
|
4016
|
+
clientMutationId,
|
|
4017
|
+
ModelClass,
|
|
4018
|
+
operation: commandType
|
|
4019
|
+
})
|
|
4020
|
+
this.setIsNewRecord(false)
|
|
4021
|
+
this._persistedAttributes = cloneFrontendModelAttributes(this.attributes())
|
|
4022
|
+
this._pendingNestedAttributes = {}
|
|
4023
|
+
this._clearPendingAttachments()
|
|
4024
|
+
|
|
4025
|
+
return this
|
|
4026
|
+
}
|
|
4027
|
+
|
|
3895
4028
|
const response = await ModelClass.executeCommand(commandType, payload)
|
|
3896
4029
|
|
|
3897
4030
|
this.assignAttributes(ModelClass.attributesFromResponse(response))
|
|
@@ -3944,9 +4077,20 @@ export default class FrontendModelBase {
|
|
|
3944
4077
|
*/
|
|
3945
4078
|
async destroy() {
|
|
3946
4079
|
const ModelClass = frontendModelClassFor(this)
|
|
4080
|
+
const id = this.primaryKeyValue()
|
|
4081
|
+
|
|
4082
|
+
if (shouldQueueFrontendModelOperationOffline(ModelClass, "destroy")) {
|
|
4083
|
+
await queueFrontendModelMutationOffline({
|
|
4084
|
+
attributes: {[ModelClass.primaryKey()]: id},
|
|
4085
|
+
ModelClass,
|
|
4086
|
+
operation: "destroy"
|
|
4087
|
+
})
|
|
4088
|
+
|
|
4089
|
+
return
|
|
4090
|
+
}
|
|
3947
4091
|
|
|
3948
4092
|
await ModelClass.executeCommand("destroy", {
|
|
3949
|
-
id
|
|
4093
|
+
id
|
|
3950
4094
|
})
|
|
3951
4095
|
}
|
|
3952
4096
|
|
|
@@ -129,14 +129,14 @@ export function normalizePreload(preload) {
|
|
|
129
129
|
continue
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
throw
|
|
132
|
+
throw frontendModelQueryError(`Invalid preload entry type: ${typeof entry}`)
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
return normalized
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
if (!isPlainObject(preload)) {
|
|
139
|
-
throw
|
|
139
|
+
throw frontendModelQueryError(`Invalid preload type: ${typeof preload}`)
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
/**
|
|
@@ -155,7 +155,7 @@ export function normalizePreload(preload) {
|
|
|
155
155
|
continue
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
throw
|
|
158
|
+
throw frontendModelQueryError(`Invalid preload value for ${relationshipName}: ${typeof relationshipPreload}`)
|
|
159
159
|
}
|
|
160
160
|
|
|
161
161
|
return normalized
|
|
@@ -6,6 +6,28 @@ import restArgsError from "../utils/rest-args-error.js"
|
|
|
6
6
|
import {validateFrontendModelResourceCommandName} from "./resource-config-validation.js"
|
|
7
7
|
|
|
8
8
|
const BASE_FRONTEND_MODEL_ABILITY_ACTIONS = ["create", "destroy", "read", "update"]
|
|
9
|
+
const SHA256_INITIAL_HASH = [
|
|
10
|
+
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
|
|
11
|
+
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
|
|
12
|
+
]
|
|
13
|
+
const SHA256_K = [
|
|
14
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
|
|
15
|
+
0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
16
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
|
|
17
|
+
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
18
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
|
|
19
|
+
0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
20
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
|
|
21
|
+
0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
22
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
|
|
23
|
+
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
24
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
|
|
25
|
+
0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
26
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
|
|
27
|
+
0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
28
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
|
|
29
|
+
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
|
|
30
|
+
]
|
|
9
31
|
const RESOURCE_STATIC_CONFIG_KEYS = new Set([
|
|
10
32
|
"abilities",
|
|
11
33
|
"attachments",
|
|
@@ -21,6 +43,7 @@ const RESOURCE_STATIC_CONFIG_KEYS = new Set([
|
|
|
21
43
|
"relationships",
|
|
22
44
|
"server",
|
|
23
45
|
"SharedResource",
|
|
46
|
+
"sync",
|
|
24
47
|
"translatedAttributes"
|
|
25
48
|
])
|
|
26
49
|
|
|
@@ -157,7 +180,8 @@ function normalizeFrontendModelResourceConfiguration(resourceConfiguration) {
|
|
|
157
180
|
"modelName",
|
|
158
181
|
"primaryKey",
|
|
159
182
|
"relationships",
|
|
160
|
-
"server"
|
|
183
|
+
"server",
|
|
184
|
+
"sync"
|
|
161
185
|
]) {
|
|
162
186
|
delete restArgs[key]
|
|
163
187
|
}
|
|
@@ -165,6 +189,7 @@ function normalizeFrontendModelResourceConfiguration(resourceConfiguration) {
|
|
|
165
189
|
restArgsError(restArgs)
|
|
166
190
|
|
|
167
191
|
const normalizedCommands = normalizeFrontendModelResourceCommands(resourceConfiguration)
|
|
192
|
+
const sync = normalizeFrontendModelResourceSync(resourceConfiguration)
|
|
168
193
|
|
|
169
194
|
return {
|
|
170
195
|
...resourceConfiguration,
|
|
@@ -176,7 +201,8 @@ function normalizeFrontendModelResourceConfiguration(resourceConfiguration) {
|
|
|
176
201
|
// name, derived from `{name, args?, returnType?}` command entries. The
|
|
177
202
|
// generator uses it to type each custom command method.
|
|
178
203
|
commandMetadata: normalizedCommands.commandMetadata,
|
|
179
|
-
memberCommands: normalizedCommands.memberCommands
|
|
204
|
+
memberCommands: normalizedCommands.memberCommands,
|
|
205
|
+
sync
|
|
180
206
|
}
|
|
181
207
|
}
|
|
182
208
|
|
|
@@ -225,6 +251,282 @@ function defaultCrudAbilities() {
|
|
|
225
251
|
}
|
|
226
252
|
}
|
|
227
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Builds a frontend-safe sync manifest for all sync-enabled frontend-model resources.
|
|
256
|
+
* @param {import("../configuration-types.js").BackendProjectConfiguration[]} backendProjects - Backend projects to scan.
|
|
257
|
+
* @returns {Record<string, import("../configuration-types.js").NormalizedFrontendModelResourceSyncConfiguration>} - Sync metadata keyed by model name.
|
|
258
|
+
*/
|
|
259
|
+
export function frontendModelSyncManifestForBackendProjects(backendProjects) {
|
|
260
|
+
/** @type {Record<string, import("../configuration-types.js").NormalizedFrontendModelResourceSyncConfiguration>} */
|
|
261
|
+
const manifest = {}
|
|
262
|
+
|
|
263
|
+
for (const backendProject of backendProjects) {
|
|
264
|
+
const resources = frontendModelResourcesForBackendProject(backendProject)
|
|
265
|
+
|
|
266
|
+
for (const configuredModelName of Object.keys(resources).sort()) {
|
|
267
|
+
const resourceDefinition = resources[configuredModelName]
|
|
268
|
+
const resourceConfiguration = frontendModelResourceConfigurationFromDefinition(resourceDefinition)
|
|
269
|
+
|
|
270
|
+
if (!resourceConfiguration) continue
|
|
271
|
+
if (!resourceConfiguration.sync?.enabled) continue
|
|
272
|
+
|
|
273
|
+
const modelName = resourceConfiguration.modelName || configuredModelName
|
|
274
|
+
|
|
275
|
+
manifest[modelName] = resourceConfiguration.sync
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return manifest
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Normalizes sync policy metadata and computes a deterministic hash from safe policy inputs.
|
|
284
|
+
* @param {import("../configuration-types.js").FrontendModelResourceConfiguration} resourceConfiguration - Raw resource configuration.
|
|
285
|
+
* @returns {import("../configuration-types.js").NormalizedFrontendModelResourceSyncConfiguration | undefined} - Frontend-safe sync metadata.
|
|
286
|
+
*/
|
|
287
|
+
function normalizeFrontendModelResourceSync(resourceConfiguration) {
|
|
288
|
+
const sync = resourceConfiguration.sync
|
|
289
|
+
|
|
290
|
+
if (sync === undefined || sync === null) return undefined
|
|
291
|
+
if (sync === false) return {enabled: false, operations: [], policyHash: syncPolicyHash({enabled: false}), policyVersion: null}
|
|
292
|
+
if (sync === true) {
|
|
293
|
+
return normalizeFrontendModelResourceSync({
|
|
294
|
+
...resourceConfiguration,
|
|
295
|
+
sync: {operations: ["index", "find"]}
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
if (!sync || typeof sync !== "object" || Array.isArray(sync)) {
|
|
299
|
+
throw new Error("Resource sync configuration must be true, false, or an object.")
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const {enabled = true, metadata, operations, policy, policyVersion, ...rest} = /** @type {import("../configuration-types.js").FrontendModelResourceSyncConfiguration} */ (sync)
|
|
303
|
+
|
|
304
|
+
if (Object.keys(rest).length > 0) {
|
|
305
|
+
throw new Error(`Unexpected sync keys: ${Object.keys(rest).join(", ")}. Allowed: enabled, metadata, operations, policy, policyVersion`)
|
|
306
|
+
}
|
|
307
|
+
if (enabled !== true && enabled !== false) throw new Error("Resource sync enabled must be true or false when provided.")
|
|
308
|
+
|
|
309
|
+
const normalizedOperations = normalizeSyncOperations(operations)
|
|
310
|
+
const normalizedMetadata = metadata === undefined ? undefined : deterministicSyncJson({label: "metadata", value: metadata})
|
|
311
|
+
const normalizedPolicy = policy === undefined ? undefined : deterministicSyncJson({label: "policy", value: policy})
|
|
312
|
+
const normalizedPolicyVersion = policyVersion === undefined || policyVersion === null ? null : String(policyVersion)
|
|
313
|
+
const hashInput = {
|
|
314
|
+
enabled,
|
|
315
|
+
metadata: normalizedMetadata,
|
|
316
|
+
modelName: resourceConfiguration.modelName || null,
|
|
317
|
+
operations: normalizedOperations,
|
|
318
|
+
policy: normalizedPolicy,
|
|
319
|
+
policyVersion: normalizedPolicyVersion
|
|
320
|
+
}
|
|
321
|
+
/** @type {import("../configuration-types.js").NormalizedFrontendModelResourceSyncConfiguration} */
|
|
322
|
+
const normalized = {
|
|
323
|
+
enabled,
|
|
324
|
+
operations: normalizedOperations,
|
|
325
|
+
policyHash: syncPolicyHash(hashInput),
|
|
326
|
+
policyVersion: normalizedPolicyVersion
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (normalizedMetadata !== undefined) normalized.metadata = /** @type {Record<string, import("../configuration-types.js").FrontendModelSyncJsonValue>} */ (normalizedMetadata)
|
|
330
|
+
|
|
331
|
+
return normalized
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Normalizes sync operations into a stable, duplicate-free list.
|
|
336
|
+
* @param {unknown} operations - Raw operations value.
|
|
337
|
+
* @returns {string[]} - Normalized operations.
|
|
338
|
+
*/
|
|
339
|
+
function normalizeSyncOperations(operations) {
|
|
340
|
+
if (operations === undefined) return []
|
|
341
|
+
if (!Array.isArray(operations)) throw new Error("Resource sync operations must be an array of operation names.")
|
|
342
|
+
|
|
343
|
+
const normalized = operations.map((operation) => {
|
|
344
|
+
if (typeof operation !== "string" || operation.length < 1) throw new Error("Resource sync operations entries must be non-empty strings.")
|
|
345
|
+
|
|
346
|
+
return operation
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
return [...new Set(normalized)].sort()
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Builds a deterministic policy hash.
|
|
354
|
+
* @param {unknown} value - Hash input.
|
|
355
|
+
* @returns {string} - sha256-prefixed hash.
|
|
356
|
+
*/
|
|
357
|
+
function syncPolicyHash(value) {
|
|
358
|
+
return `sha256-${sha256Hex(stableJsonStringify(value))}`
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Computes SHA-256 without importing Node-only crypto modules, keeping this
|
|
363
|
+
* resource-definition module safe for Expo/browser bundles.
|
|
364
|
+
* @param {string} message - UTF-8 message.
|
|
365
|
+
* @returns {string} - Hex digest.
|
|
366
|
+
*/
|
|
367
|
+
function sha256Hex(message) {
|
|
368
|
+
const bytes = utf8Bytes(message)
|
|
369
|
+
const padded = [...bytes]
|
|
370
|
+
const bitLength = bytes.length * 8
|
|
371
|
+
const hash = [...SHA256_INITIAL_HASH]
|
|
372
|
+
/** @type {number[]} */
|
|
373
|
+
const words = new Array(64)
|
|
374
|
+
|
|
375
|
+
padded.push(0x80)
|
|
376
|
+
while (padded.length % 64 !== 56) padded.push(0)
|
|
377
|
+
|
|
378
|
+
const highLength = Math.floor(bitLength / 0x100000000)
|
|
379
|
+
const lowLength = bitLength >>> 0
|
|
380
|
+
|
|
381
|
+
for (const value of [highLength, lowLength]) {
|
|
382
|
+
padded.push((value >>> 24) & 0xff, (value >>> 16) & 0xff, (value >>> 8) & 0xff, value & 0xff)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
for (let offset = 0; offset < padded.length; offset += 64) {
|
|
386
|
+
for (let i = 0; i < 16; i++) {
|
|
387
|
+
const index = offset + (i * 4)
|
|
388
|
+
|
|
389
|
+
words[i] = (((padded[index] || 0) << 24) | ((padded[index + 1] || 0) << 16) | ((padded[index + 2] || 0) << 8) | (padded[index + 3] || 0)) >>> 0
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
for (let i = 16; i < 64; i++) {
|
|
393
|
+
const s0 = rotateRight(words[i - 15], 7) ^ rotateRight(words[i - 15], 18) ^ (words[i - 15] >>> 3)
|
|
394
|
+
const s1 = rotateRight(words[i - 2], 17) ^ rotateRight(words[i - 2], 19) ^ (words[i - 2] >>> 10)
|
|
395
|
+
|
|
396
|
+
words[i] = add32(words[i - 16], s0, words[i - 7], s1)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
let [a, b, c, d, e, f, g, h] = hash
|
|
400
|
+
|
|
401
|
+
for (let i = 0; i < 64; i++) {
|
|
402
|
+
const s1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25)
|
|
403
|
+
const ch = (e & f) ^ ((~e) & g)
|
|
404
|
+
const temp1 = add32(h, s1, ch, SHA256_K[i], words[i])
|
|
405
|
+
const s0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22)
|
|
406
|
+
const maj = (a & b) ^ (a & c) ^ (b & c)
|
|
407
|
+
const temp2 = add32(s0, maj)
|
|
408
|
+
|
|
409
|
+
h = g
|
|
410
|
+
g = f
|
|
411
|
+
f = e
|
|
412
|
+
e = add32(d, temp1)
|
|
413
|
+
d = c
|
|
414
|
+
c = b
|
|
415
|
+
b = a
|
|
416
|
+
a = add32(temp1, temp2)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
hash[0] = add32(hash[0], a)
|
|
420
|
+
hash[1] = add32(hash[1], b)
|
|
421
|
+
hash[2] = add32(hash[2], c)
|
|
422
|
+
hash[3] = add32(hash[3], d)
|
|
423
|
+
hash[4] = add32(hash[4], e)
|
|
424
|
+
hash[5] = add32(hash[5], f)
|
|
425
|
+
hash[6] = add32(hash[6], g)
|
|
426
|
+
hash[7] = add32(hash[7], h)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return hash.map((value) => value.toString(16).padStart(8, "0")).join("")
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Converts a string to UTF-8 bytes.
|
|
434
|
+
* @param {string} value - String value.
|
|
435
|
+
* @returns {number[]} - UTF-8 bytes.
|
|
436
|
+
*/
|
|
437
|
+
function utf8Bytes(value) {
|
|
438
|
+
/** @type {number[]} */
|
|
439
|
+
const bytes = []
|
|
440
|
+
|
|
441
|
+
for (const character of value) {
|
|
442
|
+
const codePoint = /** @type {number} */ (character.codePointAt(0))
|
|
443
|
+
|
|
444
|
+
if (codePoint <= 0x7f) {
|
|
445
|
+
bytes.push(codePoint)
|
|
446
|
+
} else if (codePoint <= 0x7ff) {
|
|
447
|
+
bytes.push(0xc0 | (codePoint >>> 6), 0x80 | (codePoint & 0x3f))
|
|
448
|
+
} else if (codePoint <= 0xffff) {
|
|
449
|
+
bytes.push(0xe0 | (codePoint >>> 12), 0x80 | ((codePoint >>> 6) & 0x3f), 0x80 | (codePoint & 0x3f))
|
|
450
|
+
} else {
|
|
451
|
+
bytes.push(0xf0 | (codePoint >>> 18), 0x80 | ((codePoint >>> 12) & 0x3f), 0x80 | ((codePoint >>> 6) & 0x3f), 0x80 | (codePoint & 0x3f))
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return bytes
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Adds unsigned 32-bit integers.
|
|
460
|
+
* @param {...number} values - Values to add.
|
|
461
|
+
* @returns {number} - Unsigned 32-bit result.
|
|
462
|
+
*/
|
|
463
|
+
function add32(...values) {
|
|
464
|
+
return values.reduce((sum, value) => (sum + value) >>> 0, 0)
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Rotates a 32-bit integer right.
|
|
469
|
+
* @param {number} value - Value to rotate.
|
|
470
|
+
* @param {number} bits - Bit count.
|
|
471
|
+
* @returns {number} - Rotated value.
|
|
472
|
+
*/
|
|
473
|
+
function rotateRight(value, bits) {
|
|
474
|
+
return (value >>> bits) | (value << (32 - bits))
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Validates that a sync config subtree is deterministic JSON and does not contain obvious secrets.
|
|
479
|
+
* @param {object} args - Arguments.
|
|
480
|
+
* @param {string} args.label - Diagnostic path label.
|
|
481
|
+
* @param {unknown} args.value - Value to validate.
|
|
482
|
+
* @returns {import("../configuration-types.js").FrontendModelSyncJsonValue} - Stable JSON value.
|
|
483
|
+
*/
|
|
484
|
+
function deterministicSyncJson({label, value}) {
|
|
485
|
+
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value
|
|
486
|
+
|
|
487
|
+
if (Array.isArray(value)) {
|
|
488
|
+
return value.map((entry, index) => deterministicSyncJson({label: `${label}/${index}`, value: entry}))
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (value && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) {
|
|
492
|
+
/** @type {Record<string, import("../configuration-types.js").FrontendModelSyncJsonValue>} */
|
|
493
|
+
const normalized = {}
|
|
494
|
+
|
|
495
|
+
for (const key of Object.keys(value).sort()) {
|
|
496
|
+
const childValue = /** @type {Record<string, unknown>} */ (value)[key]
|
|
497
|
+
|
|
498
|
+
if (childValue === undefined) continue
|
|
499
|
+
if (syncConfigKeyLooksSecret(key)) {
|
|
500
|
+
throw new Error(`Sync policy ${label}/${key} is not allowed in frontend-visible sync policy config`)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
normalized[key] = deterministicSyncJson({label: `${label}/${key}`, value: childValue})
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return normalized
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
throw new Error("Sync policy input must be deterministic JSON")
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Stable JSON stringifier with sorted object keys.
|
|
514
|
+
* @param {unknown} value - Value to stringify.
|
|
515
|
+
* @returns {string} - Stable JSON.
|
|
516
|
+
*/
|
|
517
|
+
function stableJsonStringify(value) {
|
|
518
|
+
return JSON.stringify(deterministicSyncJson({label: "hash", value}))
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Returns whether a sync config key looks like a credential/secret.
|
|
523
|
+
* @param {string} key - Object key.
|
|
524
|
+
* @returns {boolean} - Whether key is disallowed.
|
|
525
|
+
*/
|
|
526
|
+
function syncConfigKeyLooksSecret(key) {
|
|
527
|
+
return /secret|token|password|private.?key|signing.?key/i.test(key)
|
|
528
|
+
}
|
|
529
|
+
|
|
228
530
|
/**
|
|
229
531
|
* Runs normalize frontend model resource commands.
|
|
230
532
|
* @param {import("../configuration-types.js").FrontendModelResourceConfiguration} resourceConfiguration - Raw resource configuration.
|
|
@@ -17,6 +17,22 @@ const FRONTEND_MODEL_CONTROLLER_PATH = new URL("../../frontend-model-controller.
|
|
|
17
17
|
export default async function frontendModelCommandRouteHook({configuration, currentPath, hasMatchingCustomRoute}) {
|
|
18
18
|
const normalizedCurrentPath = normalizePath(currentPath)
|
|
19
19
|
|
|
20
|
+
if (normalizedCurrentPath === "/frontend-models/sync/bootstrap") {
|
|
21
|
+
return {
|
|
22
|
+
action: "frontend-sync-bootstrap",
|
|
23
|
+
controller: "velocious/api",
|
|
24
|
+
controllerPath: FRONTEND_MODEL_CONTROLLER_PATH
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (normalizedCurrentPath === "/frontend-models/sync/replay") {
|
|
29
|
+
return {
|
|
30
|
+
action: "frontend-sync-replay",
|
|
31
|
+
controller: "velocious/api",
|
|
32
|
+
controllerPath: FRONTEND_MODEL_CONTROLLER_PATH
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
20
36
|
if (normalizedCurrentPath === SHARED_FRONTEND_MODEL_API_PATH) {
|
|
21
37
|
return {
|
|
22
38
|
action: "frontend-api",
|