velocious 1.0.475 → 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 +3 -0
- package/build/configuration-types.js +1 -0
- package/build/configuration.js +5 -0
- package/build/database/drivers/base.js +16 -0
- 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 +13 -0
- 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/sql/remove-index.js +6 -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/frontend-model-controller.js +275 -1
- package/build/routes/hooks/frontend-model-command-route-hook.js +8 -0
- 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.map +1 -1
- package/build/src/configuration.js +6 -1
- package/build/src/database/drivers/base.d.ts +20 -0
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +16 -1
- package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mssql/index.js +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.map +1 -1
- package/build/src/database/drivers/mysql/index.js +12 -1
- 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/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/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/frontend-model-controller.d.ts +62 -0
- package/build/src/frontend-model-controller.d.ts.map +1 -1
- package/build/src/frontend-model-controller.js +248 -2
- 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 +8 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/configuration-types.js +1 -0
- package/src/configuration.js +5 -0
- package/src/database/drivers/base.js +16 -0
- 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 +13 -0
- 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/sql/remove-index.js +6 -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/frontend-model-controller.js +275 -1
- package/src/routes/hooks/frontend-model-command-route-hook.js +8 -0
|
@@ -120,6 +120,7 @@ export default class DbGenerateModel extends BaseCommand {
|
|
|
120
120
|
|
|
121
121
|
const columns = await table.getColumns()
|
|
122
122
|
const writeAttributeTypeName = `${modelNameCamelized}WriteAttributes`
|
|
123
|
+
const belongsToWriteAttributes = await this.belongsToWriteAttributesForModel({modelClass, modelsDir})
|
|
123
124
|
const nestedWriteAttributes = this.nestedWriteAttributesForModel({modelClass})
|
|
124
125
|
|
|
125
126
|
fileContent += `import DatabaseRecord from "${velociousPath}/database/record/index.js"\n\n`
|
|
@@ -135,6 +136,9 @@ export default class DbGenerateModel extends BaseCommand {
|
|
|
135
136
|
fileContent += ` * @property {${setterJsdocType}${column.getNull() ? " | null" : ""}} [${camelizedColumnName}] - Value for the ${camelizedColumnName} attribute.\n`
|
|
136
137
|
}
|
|
137
138
|
}
|
|
139
|
+
for (const belongsToWriteAttribute of belongsToWriteAttributes) {
|
|
140
|
+
fileContent += ` * @property {${belongsToWriteAttribute.propertyType}} [${belongsToWriteAttribute.propertyName}] - Related ${belongsToWriteAttribute.relationshipName} record.\n`
|
|
141
|
+
}
|
|
138
142
|
for (const nestedWriteAttribute of nestedWriteAttributes) {
|
|
139
143
|
fileContent += ` * @property {${nestedWriteAttribute.propertyType}} [${nestedWriteAttribute.propertyName}] - Nested ${nestedWriteAttribute.relationshipName} attributes.\n`
|
|
140
144
|
}
|
|
@@ -440,6 +444,38 @@ export default class DbGenerateModel extends BaseCommand {
|
|
|
440
444
|
return this.jsDocTypeFromColumn(column, modelClass)
|
|
441
445
|
}
|
|
442
446
|
|
|
447
|
+
/**
|
|
448
|
+
* Runs belongs to write attributes for model.
|
|
449
|
+
* @param {object} args - Arguments.
|
|
450
|
+
* @param {typeof import("../../../../../database/record/index.js").default} args.modelClass - Model class.
|
|
451
|
+
* @param {string} args.modelsDir - Source models directory.
|
|
452
|
+
* @returns {Promise<Array<{propertyName: string, propertyType: string, relationshipName: string}>>} - Belongs-to write attributes.
|
|
453
|
+
*/
|
|
454
|
+
async belongsToWriteAttributesForModel({modelClass, modelsDir}) {
|
|
455
|
+
const writeAttributes = []
|
|
456
|
+
|
|
457
|
+
for (const relationship of modelClass.getRelationships()) {
|
|
458
|
+
if (relationship.getType() !== "belongsTo") continue
|
|
459
|
+
if (relationship.getPolymorphic()) continue
|
|
460
|
+
|
|
461
|
+
const targetModelClass = relationship.getTargetModelClass()
|
|
462
|
+
|
|
463
|
+
if (!targetModelClass) throw new Error(`Relationship '${relationship.getRelationshipName()}' on '${modelClass.getModelName()}' has no target model class`)
|
|
464
|
+
|
|
465
|
+
const targetModelFileName = inflection.dasherize(inflection.underscore(targetModelClass.getModelName()))
|
|
466
|
+
const targetModelPath = `${modelsDir}/${targetModelFileName}.js`
|
|
467
|
+
const targetImportPath = await fileExists(targetModelPath) ? `../models/${targetModelFileName}.js` : `./${targetModelFileName}.js`
|
|
468
|
+
|
|
469
|
+
writeAttributes.push({
|
|
470
|
+
propertyName: relationship.getRelationshipName(),
|
|
471
|
+
propertyType: `import("${targetImportPath}").default`,
|
|
472
|
+
relationshipName: relationship.getRelationshipName()
|
|
473
|
+
})
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return writeAttributes
|
|
477
|
+
}
|
|
478
|
+
|
|
443
479
|
/**
|
|
444
480
|
* Runs nested write attributes for model.
|
|
445
481
|
* @param {object} args - Arguments.
|
|
@@ -6,7 +6,8 @@ import FrontendModelBaseResource from "./frontend-model-resource/base-resource.j
|
|
|
6
6
|
import Response from "./http-server/client/response.js"
|
|
7
7
|
import {frontendModelResourcesWithBuiltInsForBackendProject} from "./frontend-models/built-in-resources.js"
|
|
8
8
|
import {frontendModelResourceClassFromDefinition, frontendModelResourceConfigurationFromDefinition, frontendModelResourcePath, frontendModelResourcesForBackendProject, frontendModelSyncManifestForBackendProjects} from "./frontend-models/resource-definition.js"
|
|
9
|
-
import {createOfflineGrantFromBootstrap} from "./sync/offline-grant.js"
|
|
9
|
+
import {createOfflineGrantFromBootstrap, verifyOfflineGrant} from "./sync/offline-grant.js"
|
|
10
|
+
import {mutationIdempotencyKey, verifySignedMutation} from "./sync/device-identity.js"
|
|
10
11
|
import {FrontendModelQueryError, normalizeGroup as normalizeQueryGroup, normalizeJoins as normalizeQueryJoins, normalizePluck as normalizeQueryPluck, normalizePreload as normalizeQueryPreload, normalizeSearchOperator as normalizeQuerySearchOperator, normalizeSort as normalizeQuerySort} from "./frontend-models/query.js"
|
|
11
12
|
import {assignSafeProperty, deserializeFrontendModelTransportValue, isBackendModelInstance, serializeFrontendModelTransportValue} from "./frontend-models/transport-serialization.js"
|
|
12
13
|
import {requestDetails} from "./error-reporting/request-details.js"
|
|
@@ -172,6 +173,19 @@ const frontendModelWhereNoMatchSymbol = Symbol("frontendModelWhereNoMatch")
|
|
|
172
173
|
const frontendModelClientSafeErrorMessage = "Request failed."
|
|
173
174
|
const frontendModelDebugErrorEnvironments = new Set(["development", "test"])
|
|
174
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Builds a client-safe sync replay validation error.
|
|
178
|
+
* @param {string} message - Client-safe validation message.
|
|
179
|
+
* @param {unknown} [cause] - Original cause.
|
|
180
|
+
* @returns {VelociousError} - Client-safe replay error.
|
|
181
|
+
*/
|
|
182
|
+
function frontendSyncReplaySafeError(message, cause) {
|
|
183
|
+
return VelociousError.safe(message, {
|
|
184
|
+
cause,
|
|
185
|
+
code: "frontend_sync_replay_error"
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
175
189
|
/**
|
|
176
190
|
* Runs frontend model query metadata.
|
|
177
191
|
* @param {import("./database/query/model-class-query.js").default} query - Query instance.
|
|
@@ -3561,6 +3575,266 @@ export default class FrontendModelController extends Controller {
|
|
|
3561
3575
|
throw new Error("Expected sync bootstrap current user")
|
|
3562
3576
|
}
|
|
3563
3577
|
|
|
3578
|
+
/**
|
|
3579
|
+
* Runs frontend sync replay.
|
|
3580
|
+
* @returns {Promise<void>} - Sync replay response with per-mutation results.
|
|
3581
|
+
*/
|
|
3582
|
+
async frontendSyncReplay() {
|
|
3583
|
+
if (this.request().httpMethod() === "OPTIONS") {
|
|
3584
|
+
await this.render({status: 204, json: {}})
|
|
3585
|
+
return
|
|
3586
|
+
}
|
|
3587
|
+
|
|
3588
|
+
const params = /** @type {Record<string, ?>} */ (deserializeFrontendModelTransportValue(this.params()))
|
|
3589
|
+
const signedMutations = this.frontendSyncReplaySignedMutations(params)
|
|
3590
|
+
const results = []
|
|
3591
|
+
|
|
3592
|
+
for (const signedMutation of signedMutations) {
|
|
3593
|
+
let idempotencyKey = null
|
|
3594
|
+
|
|
3595
|
+
try {
|
|
3596
|
+
idempotencyKey = mutationIdempotencyKey(/** @type {import("./sync/device-identity.js").SignedSyncMutation} */ (signedMutation))
|
|
3597
|
+
const response = await this.frontendSyncReplaySignedMutation(signedMutation)
|
|
3598
|
+
|
|
3599
|
+
results.push({idempotencyKey, response, status: "success"})
|
|
3600
|
+
} catch (error) {
|
|
3601
|
+
const errorContext = this.frontendModelEndpointErrorContext({
|
|
3602
|
+
action: "frontendSyncReplay",
|
|
3603
|
+
commandType: signedMutation && typeof signedMutation === "object" && "mutation" in signedMutation
|
|
3604
|
+
? /** @type {{mutation?: {operation?: ?}}} */ (signedMutation).mutation?.operation
|
|
3605
|
+
: undefined,
|
|
3606
|
+
error,
|
|
3607
|
+
model: signedMutation && typeof signedMutation === "object" && "mutation" in signedMutation
|
|
3608
|
+
? /** @type {{mutation?: {model?: ?}}} */ (signedMutation).mutation?.model
|
|
3609
|
+
: undefined
|
|
3610
|
+
})
|
|
3611
|
+
|
|
3612
|
+
await this.frontendModelLogEndpointError({
|
|
3613
|
+
action: errorContext.action,
|
|
3614
|
+
commandType: errorContext.commandType,
|
|
3615
|
+
error,
|
|
3616
|
+
model: errorContext.model
|
|
3617
|
+
})
|
|
3618
|
+
|
|
3619
|
+
results.push({
|
|
3620
|
+
idempotencyKey,
|
|
3621
|
+
response: await this.frontendModelClientErrorPayloadForError(error, errorContext),
|
|
3622
|
+
status: "error"
|
|
3623
|
+
})
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
|
|
3627
|
+
await this.render({
|
|
3628
|
+
json: /** @type {Record<string, ?>} */ (serializeFrontendModelTransportValue({
|
|
3629
|
+
results,
|
|
3630
|
+
status: "success"
|
|
3631
|
+
}, this.transportSerializationOptions()))
|
|
3632
|
+
})
|
|
3633
|
+
}
|
|
3634
|
+
|
|
3635
|
+
/**
|
|
3636
|
+
* Resolves signed replay mutations from request params.
|
|
3637
|
+
* @param {Record<string, ?>} params - Request params.
|
|
3638
|
+
* @returns {Array<?>} - Signed mutation envelopes.
|
|
3639
|
+
*/
|
|
3640
|
+
frontendSyncReplaySignedMutations(params) {
|
|
3641
|
+
if (Array.isArray(params.mutations)) return params.mutations
|
|
3642
|
+
if (params.mutation) return [params.mutation]
|
|
3643
|
+
|
|
3644
|
+
throw new Error("Expected sync replay mutation or mutations")
|
|
3645
|
+
}
|
|
3646
|
+
|
|
3647
|
+
/**
|
|
3648
|
+
* Verifies and replays one signed sync mutation.
|
|
3649
|
+
* @param {?} signedMutation - Signed mutation envelope.
|
|
3650
|
+
* @returns {Promise<Record<string, ?>>} - Frontend-model command response.
|
|
3651
|
+
*/
|
|
3652
|
+
async frontendSyncReplaySignedMutation(signedMutation) {
|
|
3653
|
+
const configuration = this.getConfiguration()
|
|
3654
|
+
const syncConfiguration = configuration.getSyncConfiguration()
|
|
3655
|
+
const backendPublicKey = syncConfiguration.deviceCertificateBackendPublicKey
|
|
3656
|
+
|
|
3657
|
+
if (!backendPublicKey) throw frontendSyncReplaySafeError("sync.deviceCertificateBackendPublicKey is required for sync replay")
|
|
3658
|
+
|
|
3659
|
+
let mutation
|
|
3660
|
+
|
|
3661
|
+
try {
|
|
3662
|
+
mutation = await verifySignedMutation({
|
|
3663
|
+
backendPublicKey,
|
|
3664
|
+
signedMutation: /** @type {import("./sync/device-identity.js").SignedSyncMutation} */ (signedMutation)
|
|
3665
|
+
})
|
|
3666
|
+
} catch (error) {
|
|
3667
|
+
throw frontendSyncReplaySafeError(error instanceof Error ? error.message : String(error), error)
|
|
3668
|
+
}
|
|
3669
|
+
|
|
3670
|
+
const syncManifest = frontendModelSyncManifestForBackendProjects(configuration.getBackendProjects())
|
|
3671
|
+
const syncResource = syncManifest[mutation.model]
|
|
3672
|
+
|
|
3673
|
+
if (!syncResource) throw frontendSyncReplaySafeError(`Sync replay model is not enabled: ${mutation.model}`)
|
|
3674
|
+
if (!syncResource.operations.includes(mutation.operation)) {
|
|
3675
|
+
throw frontendSyncReplaySafeError(`Sync replay operation is not enabled for ${mutation.model}: ${mutation.operation}`)
|
|
3676
|
+
}
|
|
3677
|
+
if (syncResource.policyHash !== mutation.policyHash) {
|
|
3678
|
+
throw frontendSyncReplaySafeError(`Sync replay policy hash mismatch for ${mutation.model}`)
|
|
3679
|
+
}
|
|
3680
|
+
if (!["create", "update", "destroy"].includes(mutation.operation)) {
|
|
3681
|
+
throw frontendSyncReplaySafeError(`Sync replay operation is not supported yet: ${mutation.operation}`)
|
|
3682
|
+
}
|
|
3683
|
+
|
|
3684
|
+
const signedOfflineGrant = this.frontendSyncReplaySignedOfflineGrant(signedMutation)
|
|
3685
|
+
const offlineGrant = await this.frontendSyncReplayVerifiedOfflineGrant({
|
|
3686
|
+
signedOfflineGrant,
|
|
3687
|
+
signingKeys: syncConfiguration.offlineGrantSigningKeys
|
|
3688
|
+
})
|
|
3689
|
+
|
|
3690
|
+
this.frontendSyncReplayValidateOfflineGrant({mutation, offlineGrant, syncResource})
|
|
3691
|
+
|
|
3692
|
+
const commandParams = await this.frontendSyncReplayCommandParams(mutation)
|
|
3693
|
+
|
|
3694
|
+
try {
|
|
3695
|
+
return await this.withFrontendModelParams(commandParams, async () => {
|
|
3696
|
+
return await this.withFrontendModelRequestContext(commandParams, this.response(), async () => {
|
|
3697
|
+
return await this.frontendModelCommandPayload(/** @type {"create" | "update" | "destroy"} */ (mutation.operation)) || this.frontendModelErrorPayload("Action halted by beforeAction.")
|
|
3698
|
+
})
|
|
3699
|
+
})
|
|
3700
|
+
} catch (error) {
|
|
3701
|
+
const errorContext = this.frontendModelEndpointErrorContext({
|
|
3702
|
+
action: "frontendSyncReplay",
|
|
3703
|
+
commandType: /** @type {"create" | "update" | "destroy"} */ (mutation.operation),
|
|
3704
|
+
error,
|
|
3705
|
+
model: mutation.model
|
|
3706
|
+
})
|
|
3707
|
+
|
|
3708
|
+
await this.frontendModelLogEndpointError({
|
|
3709
|
+
action: errorContext.action,
|
|
3710
|
+
commandType: errorContext.commandType,
|
|
3711
|
+
error,
|
|
3712
|
+
model: errorContext.model
|
|
3713
|
+
})
|
|
3714
|
+
|
|
3715
|
+
return await this.frontendModelClientErrorPayloadForError(error, errorContext)
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
|
|
3719
|
+
/**
|
|
3720
|
+
* Resolves the signed offline grant carried by a replay request.
|
|
3721
|
+
* @param {?} signedMutation - Signed mutation envelope.
|
|
3722
|
+
* @returns {?} - Signed offline grant envelope.
|
|
3723
|
+
*/
|
|
3724
|
+
frontendSyncReplaySignedOfflineGrant(signedMutation) {
|
|
3725
|
+
if (!signedMutation || typeof signedMutation !== "object" || Array.isArray(signedMutation)) {
|
|
3726
|
+
throw frontendSyncReplaySafeError("Expected sync replay signed offline grant")
|
|
3727
|
+
}
|
|
3728
|
+
|
|
3729
|
+
const signedMutationRecord = /** @type {Record<string, ?>} */ (signedMutation)
|
|
3730
|
+
const signedOfflineGrant = signedMutationRecord.signedOfflineGrant || signedMutationRecord.offlineGrant || signedMutationRecord.signedGrant
|
|
3731
|
+
|
|
3732
|
+
if (!signedOfflineGrant) throw frontendSyncReplaySafeError("Expected sync replay signed offline grant")
|
|
3733
|
+
|
|
3734
|
+
return signedOfflineGrant
|
|
3735
|
+
}
|
|
3736
|
+
|
|
3737
|
+
/**
|
|
3738
|
+
* Verifies a sync replay signed offline grant.
|
|
3739
|
+
* @param {object} args - Arguments.
|
|
3740
|
+
* @param {?} args.signedOfflineGrant - Signed offline grant envelope.
|
|
3741
|
+
* @param {import("./sync/offline-grant.js").OfflineGrantSigningKey[]} args.signingKeys - Available signing keys.
|
|
3742
|
+
* @returns {Promise<import("./sync/offline-grant.js").OfflineGrant>} - Verified offline grant.
|
|
3743
|
+
*/
|
|
3744
|
+
async frontendSyncReplayVerifiedOfflineGrant({signedOfflineGrant, signingKeys}) {
|
|
3745
|
+
try {
|
|
3746
|
+
return await verifyOfflineGrant({
|
|
3747
|
+
now: new Date(),
|
|
3748
|
+
signedGrant: /** @type {import("./sync/offline-grant.js").SignedOfflineGrant} */ (signedOfflineGrant),
|
|
3749
|
+
signingKeys
|
|
3750
|
+
})
|
|
3751
|
+
} catch (error) {
|
|
3752
|
+
throw frontendSyncReplaySafeError(error instanceof Error ? error.message : String(error), error)
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3755
|
+
|
|
3756
|
+
/**
|
|
3757
|
+
* Validates that a verified offline grant authorizes a replayed mutation.
|
|
3758
|
+
* @param {object} args - Arguments.
|
|
3759
|
+
* @param {import("./sync/device-identity.js").SyncMutation} args.mutation - Verified mutation.
|
|
3760
|
+
* @param {import("./sync/offline-grant.js").OfflineGrant} args.offlineGrant - Verified grant.
|
|
3761
|
+
* @param {Record<string, ?>} args.syncResource - Current sync resource entry.
|
|
3762
|
+
* @returns {void} - Throws when unauthorized.
|
|
3763
|
+
*/
|
|
3764
|
+
frontendSyncReplayValidateOfflineGrant({mutation, offlineGrant, syncResource}) {
|
|
3765
|
+
if (offlineGrant.grantId !== mutation.offlineGrantId) {
|
|
3766
|
+
throw frontendSyncReplaySafeError("Sync replay offline grant does not match mutation")
|
|
3767
|
+
}
|
|
3768
|
+
if (offlineGrant.deviceId !== mutation.actorDeviceId) {
|
|
3769
|
+
throw frontendSyncReplaySafeError("Sync replay offline grant device does not match mutation")
|
|
3770
|
+
}
|
|
3771
|
+
if (offlineGrant.userId !== mutation.actorUserId) {
|
|
3772
|
+
throw frontendSyncReplaySafeError("Sync replay offline grant user does not match mutation")
|
|
3773
|
+
}
|
|
3774
|
+
|
|
3775
|
+
const grantResource = /** @type {Record<string, ?> | undefined} */ (offlineGrant.resources[mutation.model])
|
|
3776
|
+
const grantOperations = Array.isArray(grantResource?.operations) ? grantResource.operations : []
|
|
3777
|
+
const grantPolicyHash = grantResource?.policyHash
|
|
3778
|
+
|
|
3779
|
+
if (!grantResource || grantResource.enabled !== true) throw frontendSyncReplaySafeError(`Sync replay offline grant does not authorize ${mutation.model}`)
|
|
3780
|
+
if (!grantOperations.includes(mutation.operation)) {
|
|
3781
|
+
throw frontendSyncReplaySafeError(`Sync replay offline grant does not authorize ${mutation.model}: ${mutation.operation}`)
|
|
3782
|
+
}
|
|
3783
|
+
if (grantPolicyHash !== mutation.policyHash || grantPolicyHash !== syncResource.policyHash) {
|
|
3784
|
+
throw frontendSyncReplaySafeError(`Sync replay offline grant policy hash mismatch for ${mutation.model}`)
|
|
3785
|
+
}
|
|
3786
|
+
if (!offlineGrant.scopes || typeof offlineGrant.scopes !== "object" || Array.isArray(offlineGrant.scopes)) {
|
|
3787
|
+
throw frontendSyncReplaySafeError("Sync replay offline grant scopes are invalid")
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
|
|
3791
|
+
/**
|
|
3792
|
+
* Builds frontend-model command params for a verified replay mutation.
|
|
3793
|
+
* @param {import("./sync/device-identity.js").SyncMutation} mutation - Verified mutation.
|
|
3794
|
+
* @returns {Promise<Record<string, ?>>} - Frontend-model command params.
|
|
3795
|
+
*/
|
|
3796
|
+
async frontendSyncReplayCommandParams(mutation) {
|
|
3797
|
+
const payload = mutation.payload && typeof mutation.payload === "object" && !Array.isArray(mutation.payload) ? mutation.payload : {}
|
|
3798
|
+
const {attributes, primaryKeyValue} = await this.frontendSyncReplayCommandAttributes(mutation)
|
|
3799
|
+
const commandParams = /** @type {Record<string, ?>} */ ({
|
|
3800
|
+
...payload,
|
|
3801
|
+
attributes,
|
|
3802
|
+
model: mutation.model
|
|
3803
|
+
})
|
|
3804
|
+
|
|
3805
|
+
if (mutation.operation !== "create") {
|
|
3806
|
+
const id = commandParams.id || commandParams.recordId || primaryKeyValue
|
|
3807
|
+
|
|
3808
|
+
if (typeof id !== "string" && typeof id !== "number") throw frontendSyncReplaySafeError(`Sync replay ${mutation.operation} requires an id`)
|
|
3809
|
+
|
|
3810
|
+
commandParams.id = id
|
|
3811
|
+
}
|
|
3812
|
+
|
|
3813
|
+
return commandParams
|
|
3814
|
+
}
|
|
3815
|
+
|
|
3816
|
+
/**
|
|
3817
|
+
* Resolves command attributes and primary key from a replay mutation.
|
|
3818
|
+
* @param {import("./sync/device-identity.js").SyncMutation} mutation - Verified mutation.
|
|
3819
|
+
* @returns {Promise<{attributes: Record<string, ?>, primaryKeyValue: string | number | undefined}>} - Command attributes and primary key value.
|
|
3820
|
+
*/
|
|
3821
|
+
async frontendSyncReplayCommandAttributes(mutation) {
|
|
3822
|
+
const attributes = /** @type {Record<string, ?>} */ ({...(mutation.attributes || {})})
|
|
3823
|
+
const frontendModelResource = this.getConfiguration().getBackendProjects()
|
|
3824
|
+
.map((backendProject) => this.frontendModelResourceConfigurationForBackendProjectModelName({backendProject, modelName: mutation.model}))
|
|
3825
|
+
.find((resourceConfiguration) => resourceConfiguration)
|
|
3826
|
+
|
|
3827
|
+
if (!frontendModelResource) return {attributes, primaryKeyValue: undefined}
|
|
3828
|
+
|
|
3829
|
+
const primaryKey = typeof frontendModelResource.resourceConfiguration.primaryKey === "string" ? frontendModelResource.resourceConfiguration.primaryKey : "id"
|
|
3830
|
+
const primaryKeyAttribute = attributes[primaryKey]
|
|
3831
|
+
const primaryKeyValue = typeof primaryKeyAttribute === "string" || typeof primaryKeyAttribute === "number" ? primaryKeyAttribute : undefined
|
|
3832
|
+
|
|
3833
|
+
if (primaryKeyValue !== undefined && mutation.operation !== "create") delete attributes[primaryKey]
|
|
3834
|
+
|
|
3835
|
+
return {attributes, primaryKeyValue}
|
|
3836
|
+
}
|
|
3837
|
+
|
|
3564
3838
|
/**
|
|
3565
3839
|
* Runs frontend api.
|
|
3566
3840
|
* @returns {Promise<void>} - Shared frontend model API action with batch support.
|
|
@@ -25,6 +25,14 @@ export default async function frontendModelCommandRouteHook({configuration, curr
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
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
|
+
|
|
28
36
|
if (normalizedCurrentPath === SHARED_FRONTEND_MODEL_API_PATH) {
|
|
29
37
|
return {
|
|
30
38
|
action: "frontend-api",
|