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
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import QueryBase from "./base.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* RemoveIndexBaseArgsType type.
|
|
7
|
+
* @typedef {object} RemoveIndexBaseArgsType
|
|
8
|
+
* @property {import("../drivers/base.js").default} driver - Database driver used to generate SQL.
|
|
9
|
+
* @property {string} name - Index name to drop.
|
|
10
|
+
* @property {string} tableName - Name of the table the index belongs to.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export default class VelociousDatabaseQueryRemoveIndexBase extends QueryBase {
|
|
14
|
+
/**
|
|
15
|
+
* Runs constructor.
|
|
16
|
+
* @param {RemoveIndexBaseArgsType} args - Options object.
|
|
17
|
+
*/
|
|
18
|
+
constructor({driver, name, tableName}) {
|
|
19
|
+
super({driver})
|
|
20
|
+
this.name = name
|
|
21
|
+
this.tableName = tableName
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Runs to sqls.
|
|
26
|
+
* @returns {Promise<string[]>} - Resolves with SQL statements.
|
|
27
|
+
*/
|
|
28
|
+
async toSQLs() {
|
|
29
|
+
const databaseType = this.getDriver().getType()
|
|
30
|
+
const options = this.getOptions()
|
|
31
|
+
let sql = `DROP INDEX ${options.quoteIndexName(this.name)}`
|
|
32
|
+
|
|
33
|
+
if (databaseType == "mssql" || databaseType == "mysql") {
|
|
34
|
+
sql += ` ON ${options.quoteTableName(this.tableName)}`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return [sql]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -690,11 +690,14 @@ class VelociousDatabaseRecord {
|
|
|
690
690
|
return await this.relationshipOrLoad(relationshipName, {preloadTranslations: true})
|
|
691
691
|
}
|
|
692
692
|
|
|
693
|
-
prototype[`set${inflection.camelize(relationshipName)}`] = function(/** @type {
|
|
693
|
+
prototype[`set${inflection.camelize(relationshipName)}`] = function(/** @type {VelociousDatabaseRecord | null | undefined} */ model) {
|
|
694
694
|
const relationship = this.getRelationshipByName(relationshipName)
|
|
695
|
+
const foreignKeyValue = this._belongsToForeignKeyValue({model, relationship})
|
|
695
696
|
|
|
696
|
-
relationship.setLoaded(model)
|
|
697
|
+
relationship.setLoaded(model || undefined)
|
|
698
|
+
relationship.setPreloaded(true)
|
|
697
699
|
relationship.setDirty(true)
|
|
700
|
+
this._setColumnAttribute(relationship.getForeignKey(), foreignKeyValue)
|
|
698
701
|
}
|
|
699
702
|
} else if (actualData.type == "hasMany") {
|
|
700
703
|
relationship = new HasManyRelationship(actualData)
|
|
@@ -1783,6 +1786,20 @@ class VelociousDatabaseRecord {
|
|
|
1783
1786
|
return loaded.readColumn(relationship.getPrimaryKey()) == normalizedValue
|
|
1784
1787
|
}
|
|
1785
1788
|
|
|
1789
|
+
/**
|
|
1790
|
+
* Returns the foreign key value for a belongs-to relationship assignment.
|
|
1791
|
+
* @param {object} args - Relationship assignment arguments.
|
|
1792
|
+
* @param {VelociousDatabaseRecord | null | undefined} args.model - Assigned model.
|
|
1793
|
+
* @param {import("./instance-relationships/base.js").default} args.relationship - Belongs-to relationship instance.
|
|
1794
|
+
* @returns {string | number | null | undefined} - Foreign key value for the assignment.
|
|
1795
|
+
*/
|
|
1796
|
+
_belongsToForeignKeyValue({model, relationship}) {
|
|
1797
|
+
if (model == null) return null
|
|
1798
|
+
if (!(model instanceof VelociousDatabaseRecord)) throw new Error(`Unexpected model type: ${typeof model}`)
|
|
1799
|
+
|
|
1800
|
+
return /** @type {string | number | null | undefined} */ (model.readColumn(relationship.getPrimaryKey()))
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1786
1803
|
/**
|
|
1787
1804
|
* Runs clear loaded belongs to relationship.
|
|
1788
1805
|
* @param {?} relationship - Relationship instance.
|
|
@@ -2364,8 +2381,9 @@ class VelociousDatabaseRecord {
|
|
|
2364
2381
|
await model.save()
|
|
2365
2382
|
|
|
2366
2383
|
const foreignKey = this._relationshipForeignKeyAttribute(instanceRelationship)
|
|
2384
|
+
const foreignKeyValue = this._belongsToForeignKeyValue({model, relationship: instanceRelationship})
|
|
2367
2385
|
|
|
2368
|
-
this.setAttribute(foreignKey,
|
|
2386
|
+
this.setAttribute(foreignKey, foreignKeyValue)
|
|
2369
2387
|
|
|
2370
2388
|
instanceRelationship.setPreloaded(true)
|
|
2371
2389
|
instanceRelationship.setDirty(false)
|
|
@@ -3801,14 +3819,12 @@ class VelociousDatabaseRecord {
|
|
|
3801
3819
|
const relationship = this._instanceRelationships[relationshipName]
|
|
3802
3820
|
|
|
3803
3821
|
if (relationship.getType() == "belongsTo" && relationship.getDirty()) {
|
|
3804
|
-
const model = relationship.
|
|
3822
|
+
const model = relationship.getLoadedOrUndefined()
|
|
3805
3823
|
|
|
3806
3824
|
if (model) {
|
|
3807
|
-
if (model
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
throw new Error(`Unexpected model type: ${typeof model}`)
|
|
3811
|
-
}
|
|
3825
|
+
if (Array.isArray(model)) throw new Error("Unexpected belongs-to model array")
|
|
3826
|
+
|
|
3827
|
+
belongsToChanges[relationship.getForeignKey()] = this._belongsToForeignKeyValue({model, relationship})
|
|
3812
3828
|
}
|
|
3813
3829
|
}
|
|
3814
3830
|
}
|
|
@@ -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.
|
|
@@ -442,6 +442,13 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
442
442
|
}
|
|
443
443
|
fileContent += " },\n"
|
|
444
444
|
}
|
|
445
|
+
if (modelConfig.sync?.enabled) {
|
|
446
|
+
fileContent += this.formattedJsonProperty({
|
|
447
|
+
indent: " ",
|
|
448
|
+
propertyName: "sync",
|
|
449
|
+
value: modelConfig.sync
|
|
450
|
+
})
|
|
451
|
+
}
|
|
445
452
|
fileContent += " }\n"
|
|
446
453
|
fileContent += " }\n"
|
|
447
454
|
|
|
@@ -999,6 +1006,62 @@ export default class DbGenerateFrontendModels extends BaseCommand {
|
|
|
999
1006
|
return output
|
|
1000
1007
|
}
|
|
1001
1008
|
|
|
1009
|
+
/**
|
|
1010
|
+
* Runs formatted JSON property.
|
|
1011
|
+
* @param {object} args - Formatting args.
|
|
1012
|
+
* @param {string} args.indent - Base indentation.
|
|
1013
|
+
* @param {string} args.propertyName - Object property name.
|
|
1014
|
+
* @param {unknown} args.value - JSON-compatible value.
|
|
1015
|
+
* @returns {string} - Formatted property.
|
|
1016
|
+
*/
|
|
1017
|
+
formattedJsonProperty({indent, propertyName, value}) {
|
|
1018
|
+
return `${indent}${propertyName}: ${this.formattedJsonValue({indent, value})},\n`
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/**
|
|
1022
|
+
* Runs formatted JSON value.
|
|
1023
|
+
* @param {object} args - Formatting args.
|
|
1024
|
+
* @param {string} args.indent - Indentation before this value.
|
|
1025
|
+
* @param {unknown} args.value - JSON-compatible value.
|
|
1026
|
+
* @returns {string} - Formatted value.
|
|
1027
|
+
*/
|
|
1028
|
+
formattedJsonValue({indent, value}) {
|
|
1029
|
+
if (Array.isArray(value)) {
|
|
1030
|
+
let output = "[\n"
|
|
1031
|
+
|
|
1032
|
+
for (const entry of value) {
|
|
1033
|
+
output += `${indent} ${this.formattedJsonValue({indent: `${indent} `, value: entry})},\n`
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
output += `${indent}]`
|
|
1037
|
+
|
|
1038
|
+
return output
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
if (value && typeof value === "object") {
|
|
1042
|
+
let output = "{\n"
|
|
1043
|
+
|
|
1044
|
+
for (const key of Object.keys(value)) {
|
|
1045
|
+
output += `${indent} ${this.formattedObjectKey(key)}: ${this.formattedJsonValue({indent: `${indent} `, value: /** @type {Record<string, unknown>} */ (value)[key]})},\n`
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
output += `${indent}}`
|
|
1049
|
+
|
|
1050
|
+
return output
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
return JSON.stringify(value)
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* Runs formatted object key.
|
|
1058
|
+
* @param {string} key - Object key.
|
|
1059
|
+
* @returns {string} - JavaScript object key.
|
|
1060
|
+
*/
|
|
1061
|
+
formattedObjectKey(key) {
|
|
1062
|
+
return /^[A-Za-z_$][\w$]*$/.test(key) ? key : JSON.stringify(key)
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1002
1065
|
/**
|
|
1003
1066
|
* Runs attribute definitions for model.
|
|
1004
1067
|
* @param {object} args - Arguments.
|
|
@@ -5,7 +5,9 @@ import Controller from "./controller.js"
|
|
|
5
5
|
import FrontendModelBaseResource from "./frontend-model-resource/base-resource.js"
|
|
6
6
|
import Response from "./http-server/client/response.js"
|
|
7
7
|
import {frontendModelResourcesWithBuiltInsForBackendProject} from "./frontend-models/built-in-resources.js"
|
|
8
|
-
import {frontendModelResourceClassFromDefinition, frontendModelResourceConfigurationFromDefinition, frontendModelResourcePath, frontendModelResourcesForBackendProject} from "./frontend-models/resource-definition.js"
|
|
8
|
+
import {frontendModelResourceClassFromDefinition, frontendModelResourceConfigurationFromDefinition, frontendModelResourcePath, frontendModelResourcesForBackendProject, frontendModelSyncManifestForBackendProjects} from "./frontend-models/resource-definition.js"
|
|
9
|
+
import {createOfflineGrantFromBootstrap, verifyOfflineGrant} from "./sync/offline-grant.js"
|
|
10
|
+
import {mutationIdempotencyKey, verifySignedMutation} from "./sync/device-identity.js"
|
|
9
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"
|
|
10
12
|
import {assignSafeProperty, deserializeFrontendModelTransportValue, isBackendModelInstance, serializeFrontendModelTransportValue} from "./frontend-models/transport-serialization.js"
|
|
11
13
|
import {requestDetails} from "./error-reporting/request-details.js"
|
|
@@ -25,7 +27,11 @@ import {RansackQueryError, normalizeRansackGroup, parseRansackSort} from "./util
|
|
|
25
27
|
function normalizeFrontendModelPreload(preload) {
|
|
26
28
|
if (!preload) return null
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
try {
|
|
31
|
+
return normalizeQueryPreload(preload)
|
|
32
|
+
} catch (error) {
|
|
33
|
+
throwFrontendModelQueryErrorForParserError(error)
|
|
34
|
+
}
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
/**
|
|
@@ -167,6 +173,19 @@ const frontendModelWhereNoMatchSymbol = Symbol("frontendModelWhereNoMatch")
|
|
|
167
173
|
const frontendModelClientSafeErrorMessage = "Request failed."
|
|
168
174
|
const frontendModelDebugErrorEnvironments = new Set(["development", "test"])
|
|
169
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
|
+
|
|
170
189
|
/**
|
|
171
190
|
* Runs frontend model query metadata.
|
|
172
191
|
* @param {import("./database/query/model-class-query.js").default} query - Query instance.
|
|
@@ -863,13 +882,31 @@ export default class FrontendModelController extends Controller {
|
|
|
863
882
|
for (const [relationshipName, relationshipPreload] of Object.entries(preload)) {
|
|
864
883
|
if (relationshipPreload === false) continue
|
|
865
884
|
|
|
866
|
-
const relationship = modelClass.
|
|
885
|
+
const relationship = modelClass.getRelationshipsMap()[relationshipName]
|
|
886
|
+
if (!relationship) {
|
|
887
|
+
throw frontendModelQueryError(`Unknown preload relationship "${relationshipName}" for ${modelClass.name}`)
|
|
888
|
+
}
|
|
889
|
+
|
|
867
890
|
const targetModelClass = await this.ensureFrontendModelRelationshipTargetClassInitialized({
|
|
868
891
|
backendProject,
|
|
869
892
|
relationship
|
|
870
893
|
})
|
|
871
894
|
|
|
872
|
-
if (!targetModelClass
|
|
895
|
+
if (!targetModelClass) {
|
|
896
|
+
if (isPlainObject(relationshipPreload) && Object.keys(relationshipPreload).length > 0) {
|
|
897
|
+
let message = `Cannot preload nested relationships through relationship "${relationshipName}" for ${modelClass.name} because its target model class could not be resolved`
|
|
898
|
+
|
|
899
|
+
if (relationship.getPolymorphic() && relationship.getType() === "belongsTo") {
|
|
900
|
+
message = `Cannot preload nested relationships through polymorphic relationship "${relationshipName}" for ${modelClass.name}`
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
throw frontendModelQueryError(message)
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
continue
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
if (!isPlainObject(relationshipPreload)) continue
|
|
873
910
|
|
|
874
911
|
await this.ensureFrontendModelPreloadClassesInitialized({
|
|
875
912
|
backendProject,
|
|
@@ -3438,6 +3475,366 @@ export default class FrontendModelController extends Controller {
|
|
|
3438
3475
|
return {status: "success"}
|
|
3439
3476
|
}
|
|
3440
3477
|
|
|
3478
|
+
/**
|
|
3479
|
+
* Runs frontend sync bootstrap.
|
|
3480
|
+
* @returns {Promise<void>} - Sync bootstrap response with manifest and signed offline grant.
|
|
3481
|
+
*/
|
|
3482
|
+
async frontendSyncBootstrap() {
|
|
3483
|
+
if (this.request().httpMethod() === "OPTIONS") {
|
|
3484
|
+
await this.render({status: 204, json: {}})
|
|
3485
|
+
return
|
|
3486
|
+
}
|
|
3487
|
+
|
|
3488
|
+
const params = /** @type {Record<string, import("./configuration-types.js").FrontendModelSyncJsonValue | undefined>} */ (deserializeFrontendModelTransportValue(this.params()))
|
|
3489
|
+
const configuration = this.getConfiguration()
|
|
3490
|
+
const syncManifest = frontendModelSyncManifestForBackendProjects(configuration.getBackendProjects())
|
|
3491
|
+
const offlineGrant = await createOfflineGrantFromBootstrap({
|
|
3492
|
+
deviceId: this.frontendSyncBootstrapDeviceId(params),
|
|
3493
|
+
grantId: this.frontendSyncBootstrapGrantId(params),
|
|
3494
|
+
grantTtlMs: configuration.getSyncConfiguration().offlineGrantTtlMs,
|
|
3495
|
+
now: this.frontendSyncBootstrapNow(params),
|
|
3496
|
+
resources: syncManifest,
|
|
3497
|
+
scopes: this.frontendSyncBootstrapScopes(params),
|
|
3498
|
+
signingKey: configuration.currentOfflineGrantSigningKey(),
|
|
3499
|
+
userId: this.frontendSyncBootstrapUserId()
|
|
3500
|
+
})
|
|
3501
|
+
|
|
3502
|
+
await this.render({
|
|
3503
|
+
json: /** @type {Record<string, ?>} */ (serializeFrontendModelTransportValue({
|
|
3504
|
+
offlineGrant,
|
|
3505
|
+
status: "success",
|
|
3506
|
+
syncManifest
|
|
3507
|
+
}, this.transportSerializationOptions()))
|
|
3508
|
+
})
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3511
|
+
/**
|
|
3512
|
+
* Resolves device id for sync bootstrap.
|
|
3513
|
+
* @param {Record<string, import("./configuration-types.js").FrontendModelSyncJsonValue | undefined>} params - Request params.
|
|
3514
|
+
* @returns {string} - Device id.
|
|
3515
|
+
*/
|
|
3516
|
+
frontendSyncBootstrapDeviceId(params) {
|
|
3517
|
+
if (typeof params.deviceId === "string" && params.deviceId.length > 0) return params.deviceId
|
|
3518
|
+
|
|
3519
|
+
throw new Error("Expected sync bootstrap deviceId")
|
|
3520
|
+
}
|
|
3521
|
+
|
|
3522
|
+
/**
|
|
3523
|
+
* Resolves grant id for sync bootstrap.
|
|
3524
|
+
* @param {Record<string, import("./configuration-types.js").FrontendModelSyncJsonValue | undefined>} params - Request params.
|
|
3525
|
+
* @returns {string | undefined} - Deterministic grant id for tests, generated id otherwise.
|
|
3526
|
+
*/
|
|
3527
|
+
frontendSyncBootstrapGrantId(params) {
|
|
3528
|
+
if (this.getConfiguration().getEnvironment() === "test" && typeof params.grantId === "string") return params.grantId
|
|
3529
|
+
|
|
3530
|
+
return undefined
|
|
3531
|
+
}
|
|
3532
|
+
|
|
3533
|
+
/**
|
|
3534
|
+
* Resolves bootstrap issue time.
|
|
3535
|
+
* @param {Record<string, import("./configuration-types.js").FrontendModelSyncJsonValue | undefined>} params - Request params.
|
|
3536
|
+
* @returns {Date} - Issue time.
|
|
3537
|
+
*/
|
|
3538
|
+
frontendSyncBootstrapNow(params) {
|
|
3539
|
+
if (this.getConfiguration().getEnvironment() === "test" && typeof params.now === "string") return new Date(params.now)
|
|
3540
|
+
|
|
3541
|
+
return new Date()
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3544
|
+
/**
|
|
3545
|
+
* Resolves sync bootstrap scopes.
|
|
3546
|
+
* @param {Record<string, import("./configuration-types.js").FrontendModelSyncJsonValue | undefined>} params - Request params.
|
|
3547
|
+
* @returns {Record<string, import("./configuration-types.js").FrontendModelSyncJsonValue>} - Grant scopes.
|
|
3548
|
+
*/
|
|
3549
|
+
frontendSyncBootstrapScopes(params) {
|
|
3550
|
+
const scopes = params.scopes
|
|
3551
|
+
|
|
3552
|
+
if (scopes && typeof scopes === "object" && !Array.isArray(scopes)) {
|
|
3553
|
+
return /** @type {Record<string, import("./configuration-types.js").FrontendModelSyncJsonValue>} */ (scopes)
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
return {}
|
|
3557
|
+
}
|
|
3558
|
+
|
|
3559
|
+
/**
|
|
3560
|
+
* Resolves current user id for sync bootstrap.
|
|
3561
|
+
* @returns {string} - User id.
|
|
3562
|
+
*/
|
|
3563
|
+
frontendSyncBootstrapUserId() {
|
|
3564
|
+
const ability = this.currentAbility()
|
|
3565
|
+
const currentUser = ability?.currentUser()
|
|
3566
|
+
|
|
3567
|
+
if (typeof currentUser === "string" || typeof currentUser === "number") return String(currentUser)
|
|
3568
|
+
if (currentUser && typeof currentUser === "object") {
|
|
3569
|
+
const userRecord = /** @type {{id?: string | number | (() => string | number)}} */ (currentUser)
|
|
3570
|
+
const idValue = typeof userRecord.id === "function" ? userRecord.id() : userRecord.id
|
|
3571
|
+
|
|
3572
|
+
if (typeof idValue === "string" || typeof idValue === "number") return String(idValue)
|
|
3573
|
+
}
|
|
3574
|
+
|
|
3575
|
+
throw new Error("Expected sync bootstrap current user")
|
|
3576
|
+
}
|
|
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
|
+
|
|
3441
3838
|
/**
|
|
3442
3839
|
* Runs frontend api.
|
|
3443
3840
|
* @returns {Promise<void>} - Shared frontend model API action with batch support.
|