velocious 1.0.473 → 1.0.475
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 +7 -2
- package/build/authorization/base-resource.js +99 -0
- package/build/configuration-types.js +41 -3
- package/build/configuration.js +41 -1
- package/build/database/drivers/base.js +51 -3
- package/build/database/drivers/mysql/index.js +74 -7
- 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/web-persistence.js +521 -0
- package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +64 -1
- package/build/frontend-model-controller.js +139 -16
- package/build/frontend-model-resource/base-resource.js +258 -46
- package/build/frontend-model-resource/velocious-attachment-resource.js +0 -3
- package/build/frontend-models/base.js +146 -2
- package/build/frontend-models/query.js +3 -3
- package/build/frontend-models/resource-definition.js +400 -23
- package/build/routes/hooks/frontend-model-command-route-hook.js +8 -0
- package/build/src/authorization/base-resource.d.ts +41 -0
- package/build/src/authorization/base-resource.d.ts.map +1 -1
- package/build/src/authorization/base-resource.js +88 -1
- package/build/src/configuration-types.d.ts +128 -5
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +38 -4
- package/build/src/configuration.d.ts +18 -1
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +37 -2
- package/build/src/database/drivers/base.d.ts +30 -0
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +47 -4
- 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 +65 -7
- 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/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/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 +54 -2
- package/build/src/frontend-model-controller.d.ts +34 -0
- package/build/src/frontend-model-controller.d.ts.map +1 -1
- package/build/src/frontend-model-controller.js +125 -17
- package/build/src/frontend-model-resource/base-resource.d.ts +69 -0
- package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
- package/build/src/frontend-model-resource/base-resource.js +247 -54
- package/build/src/frontend-model-resource/velocious-attachment-resource.d.ts +0 -2
- package/build/src/frontend-model-resource/velocious-attachment-resource.d.ts.map +1 -1
- package/build/src/frontend-model-resource/velocious-attachment-resource.js +1 -3
- 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 +355 -28
- 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/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/scripts/tensorbuzz-retry +21 -0
- package/src/authorization/base-resource.js +99 -0
- package/src/configuration-types.js +41 -3
- package/src/configuration.js +41 -1
- package/src/database/drivers/base.js +51 -3
- package/src/database/drivers/mysql/index.js +74 -7
- 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/web-persistence.js +521 -0
- package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +64 -1
- package/src/frontend-model-controller.js +139 -16
- package/src/frontend-model-resource/base-resource.js +258 -46
- package/src/frontend-model-resource/velocious-attachment-resource.js +0 -3
- package/src/frontend-models/base.js +146 -2
- package/src/frontend-models/query.js +3 -3
- package/src/frontend-models/resource-definition.js +400 -23
- package/src/routes/hooks/frontend-model-command-route-hook.js +8 -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
|
@@ -148,6 +148,8 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
148
148
|
/** @type {Record<string, ?> | undefined} */
|
|
149
149
|
static attachments = undefined
|
|
150
150
|
/** @type {string[] | undefined} */
|
|
151
|
+
static commands = undefined
|
|
152
|
+
/** @type {string[] | undefined} */
|
|
151
153
|
static collectionCommands = undefined
|
|
152
154
|
/** @type {string[] | undefined} */
|
|
153
155
|
static builtInCollectionCommands = undefined
|
|
@@ -157,8 +159,18 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
157
159
|
static builtInMemberCommands = undefined
|
|
158
160
|
/** @type {string[] | undefined} */
|
|
159
161
|
static relationships = undefined
|
|
162
|
+
/** @type {string | undefined} */
|
|
163
|
+
static modelName = undefined
|
|
164
|
+
/** @type {string | undefined} */
|
|
165
|
+
static primaryKey = undefined
|
|
166
|
+
/** @type {import("../configuration-types.js").FrontendModelResourceServerConfiguration | undefined} */
|
|
167
|
+
static server = undefined
|
|
168
|
+
/** @type {import("../configuration-types.js").FrontendModelResourceSyncConfiguration | boolean | undefined} */
|
|
169
|
+
static sync = undefined
|
|
160
170
|
/** @type {string[] | undefined} */
|
|
161
171
|
static translatedAttributes = undefined
|
|
172
|
+
/** @type {?} */
|
|
173
|
+
static SharedResource = undefined
|
|
162
174
|
|
|
163
175
|
/**
|
|
164
176
|
* Runs constructor.
|
|
@@ -179,6 +191,149 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
179
191
|
this.modelNameValue = "modelName" in args ? args.modelName : this.modelClass().getModelName()
|
|
180
192
|
this.paramsValue = "params" in args ? args.params : undefined
|
|
181
193
|
this.resourceConfigurationValue = "resourceConfiguration" in args ? args.resourceConfiguration : defaultResourceConfiguration
|
|
194
|
+
/** @type {FrontendModelBaseResource | null | undefined} */
|
|
195
|
+
this.sharedResourceInstanceValue = undefined
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Returns the configured shared resource class.
|
|
200
|
+
* @returns {?} - Shared resource class.
|
|
201
|
+
*/
|
|
202
|
+
static sharedResourceClass() {
|
|
203
|
+
return this.SharedResource
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Reads a static resource config value from the environment resource first,
|
|
208
|
+
* then from the shared resource.
|
|
209
|
+
* @param {"abilities" | "attachments" | "attributes" | "builtInCollectionCommands" | "builtInMemberCommands" | "collectionCommands" | "commands" | "memberCommands" | "modelName" | "primaryKey" | "relationships" | "server" | "sync" | "translatedAttributes"} name - Static config property name.
|
|
210
|
+
* @returns {?} - Resolved config value.
|
|
211
|
+
*/
|
|
212
|
+
static sharedResourceStaticValue(name) {
|
|
213
|
+
if (this[name] !== undefined) return this[name]
|
|
214
|
+
|
|
215
|
+
const SharedResource = /** @type {typeof FrontendModelBaseResource | undefined} */ (this.sharedResourceClass())
|
|
216
|
+
|
|
217
|
+
if (!SharedResource) return undefined
|
|
218
|
+
if (SharedResource[name] !== undefined) return SharedResource[name]
|
|
219
|
+
|
|
220
|
+
return undefined
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Resolves translated attributes from environment and shared resources.
|
|
225
|
+
* @returns {string[] | undefined} - Translated attribute names.
|
|
226
|
+
*/
|
|
227
|
+
static translatedAttributesConfig() {
|
|
228
|
+
return /** @type {string[] | undefined} */ (this.sharedResourceStaticValue("translatedAttributes"))
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Builds a resource instance for shared-resource fallback calls.
|
|
233
|
+
* @returns {FrontendModelBaseResource | null} - Shared resource instance when configured.
|
|
234
|
+
*/
|
|
235
|
+
sharedResourceInstance() {
|
|
236
|
+
if (this.sharedResourceInstanceValue !== undefined) return this.sharedResourceInstanceValue
|
|
237
|
+
|
|
238
|
+
const ResourceClass = /** @type {typeof FrontendModelBaseResource} */ (this.constructor)
|
|
239
|
+
const SharedResource = /** @type {typeof FrontendModelBaseResource | undefined} */ (ResourceClass.sharedResourceClass())
|
|
240
|
+
|
|
241
|
+
if (!SharedResource) {
|
|
242
|
+
this.sharedResourceInstanceValue = null
|
|
243
|
+
return this.sharedResourceInstanceValue
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (SharedResource === ResourceClass) {
|
|
247
|
+
throw new Error(`${ResourceClass.name}.SharedResource cannot point to itself.`)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
this.sharedResourceInstanceValue = new SharedResource({
|
|
251
|
+
ability: this.ability,
|
|
252
|
+
controller: this.controller,
|
|
253
|
+
context: this.context,
|
|
254
|
+
locals: this.locals,
|
|
255
|
+
modelClass: this.modelClass(),
|
|
256
|
+
modelName: this.modelName(),
|
|
257
|
+
params: this.params(),
|
|
258
|
+
resourceConfiguration: this.resourceConfiguration()
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
return this.sharedResourceInstanceValue
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Calls a shared-resource method only when the shared resource overrides the framework default.
|
|
266
|
+
* @param {string} methodName - Method name to resolve.
|
|
267
|
+
* @param {unknown[]} args - Method args.
|
|
268
|
+
* @returns {{called: boolean, result: unknown}} - Shared method call result.
|
|
269
|
+
*/
|
|
270
|
+
callSharedResourceMethod(methodName, args) {
|
|
271
|
+
const sharedResource = this.sharedResourceInstance()
|
|
272
|
+
|
|
273
|
+
if (!sharedResource) return {called: false, result: undefined}
|
|
274
|
+
|
|
275
|
+
const methodOwner = prototypeOwnerForMethod(sharedResource, methodName)
|
|
276
|
+
|
|
277
|
+
if (!methodOwner || methodOwner === FrontendModelBaseResource.prototype || methodOwner === AuthorizationBaseResource.prototype) {
|
|
278
|
+
return {called: false, result: undefined}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const method = /** @type {Record<string, (...methodArgs: unknown[]) => unknown>} */ (/** @type {unknown} */ (sharedResource))[methodName]
|
|
282
|
+
|
|
283
|
+
return {called: true, result: method.apply(sharedResource, args)}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Runs shared method result or a fallback callback.
|
|
288
|
+
* @template Result
|
|
289
|
+
* @param {string} methodName - Shared method name.
|
|
290
|
+
* @param {unknown[]} args - Shared method args.
|
|
291
|
+
* @param {() => Result} fallback - Fallback callback.
|
|
292
|
+
* @returns {Result} - Shared or fallback result.
|
|
293
|
+
*/
|
|
294
|
+
sharedResourceMethodOr(methodName, args, fallback) {
|
|
295
|
+
const sharedResult = this.callSharedResourceMethod(methodName, args)
|
|
296
|
+
|
|
297
|
+
if (sharedResult.called) return /** @type {Result} */ (sharedResult.result)
|
|
298
|
+
|
|
299
|
+
return fallback()
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Resolves a method on this resource or its shared fallback.
|
|
304
|
+
* @param {string} methodName - Method name.
|
|
305
|
+
* @returns {{method: (...methodArgs: unknown[]) => unknown, resource: FrontendModelBaseResource} | null} - Resolved method and receiver.
|
|
306
|
+
*/
|
|
307
|
+
resourceMethod(methodName) {
|
|
308
|
+
const ownMethod = /** @type {Record<string, unknown>} */ (/** @type {unknown} */ (this))[methodName]
|
|
309
|
+
|
|
310
|
+
if (typeof ownMethod === "function") {
|
|
311
|
+
return {
|
|
312
|
+
method: /** @type {(...methodArgs: unknown[]) => unknown} */ (ownMethod),
|
|
313
|
+
resource: this
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const sharedResource = this.sharedResourceInstance()
|
|
318
|
+
|
|
319
|
+
if (!sharedResource) return null
|
|
320
|
+
|
|
321
|
+
const sharedMethod = /** @type {Record<string, unknown>} */ (/** @type {unknown} */ (sharedResource))[methodName]
|
|
322
|
+
|
|
323
|
+
if (typeof sharedMethod !== "function") return null
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
method: /** @type {(...methodArgs: unknown[]) => unknown} */ (sharedMethod),
|
|
327
|
+
resource: sharedResource
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Runs abilities.
|
|
333
|
+
* @returns {void} - No return value.
|
|
334
|
+
*/
|
|
335
|
+
abilities() {
|
|
336
|
+
this.sharedResourceMethodOr("abilities", [], () => undefined)
|
|
182
337
|
}
|
|
183
338
|
|
|
184
339
|
/**
|
|
@@ -194,18 +349,36 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
194
349
|
* @returns {import("../configuration-types.js").FrontendModelResourceConfiguration} - Static resource config (raw user input shape; consumers normalize).
|
|
195
350
|
*/
|
|
196
351
|
static resourceConfig() {
|
|
352
|
+
const attributes = this.sharedResourceStaticValue("attributes")
|
|
353
|
+
const abilities = this.sharedResourceStaticValue("abilities")
|
|
354
|
+
const attachments = this.sharedResourceStaticValue("attachments")
|
|
355
|
+
const commands = this.sharedResourceStaticValue("commands")
|
|
356
|
+
const builtInCollectionCommands = this.sharedResourceStaticValue("builtInCollectionCommands")
|
|
357
|
+
const builtInMemberCommands = this.sharedResourceStaticValue("builtInMemberCommands")
|
|
358
|
+
const collectionCommands = this.sharedResourceStaticValue("collectionCommands")
|
|
359
|
+
const memberCommands = this.sharedResourceStaticValue("memberCommands")
|
|
360
|
+
const modelName = this.sharedResourceStaticValue("modelName")
|
|
361
|
+
const primaryKey = this.sharedResourceStaticValue("primaryKey")
|
|
362
|
+
const relationships = this.sharedResourceStaticValue("relationships")
|
|
363
|
+
const server = this.sharedResourceStaticValue("server")
|
|
364
|
+
const sync = this.sharedResourceStaticValue("sync")
|
|
197
365
|
/** @type {import("../configuration-types.js").FrontendModelResourceConfiguration} */
|
|
198
366
|
const config = {
|
|
199
|
-
attributes:
|
|
367
|
+
attributes: /** @type {Record<string, ?> | string[]} */ (attributes || [])
|
|
200
368
|
}
|
|
201
369
|
|
|
202
|
-
if (
|
|
203
|
-
if (
|
|
204
|
-
if (
|
|
205
|
-
if (
|
|
206
|
-
if (
|
|
207
|
-
if (
|
|
208
|
-
if (
|
|
370
|
+
if (abilities) config.abilities = /** @type {string[]} */ (abilities)
|
|
371
|
+
if (attachments) config.attachments = /** @type {Record<string, ?>} */ (attachments)
|
|
372
|
+
if (commands) config.commands = /** @type {string[]} */ (commands)
|
|
373
|
+
if (builtInCollectionCommands) config.builtInCollectionCommands = /** @type {string[]} */ (builtInCollectionCommands)
|
|
374
|
+
if (builtInMemberCommands) config.builtInMemberCommands = /** @type {string[]} */ (builtInMemberCommands)
|
|
375
|
+
if (collectionCommands) config.collectionCommands = /** @type {string[]} */ (collectionCommands)
|
|
376
|
+
if (memberCommands) config.memberCommands = /** @type {string[]} */ (memberCommands)
|
|
377
|
+
if (modelName) config.modelName = /** @type {string} */ (modelName)
|
|
378
|
+
if (primaryKey) config.primaryKey = /** @type {string} */ (primaryKey)
|
|
379
|
+
if (relationships) config.relationships = /** @type {string[]} */ (relationships)
|
|
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)
|
|
209
382
|
|
|
210
383
|
return config
|
|
211
384
|
}
|
|
@@ -242,6 +415,14 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
242
415
|
return /** @type {TModelClass} */ (this.modelClassValue)
|
|
243
416
|
}
|
|
244
417
|
|
|
418
|
+
/**
|
|
419
|
+
* Runs required model class for authorization helpers.
|
|
420
|
+
* @returns {TModelClass} - Backing model class.
|
|
421
|
+
*/
|
|
422
|
+
requiredModelClass() {
|
|
423
|
+
return this.modelClass()
|
|
424
|
+
}
|
|
425
|
+
|
|
245
426
|
/**
|
|
246
427
|
* Runs model name.
|
|
247
428
|
* @returns {string} - Model name.
|
|
@@ -307,9 +488,11 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
307
488
|
* @returns {Array<string | Record<string, ?>>} - Permit spec.
|
|
308
489
|
*/
|
|
309
490
|
permittedParams(arg) {
|
|
310
|
-
|
|
491
|
+
return this.sharedResourceMethodOr("permittedParams", [arg], () => {
|
|
492
|
+
void arg
|
|
311
493
|
|
|
312
|
-
|
|
494
|
+
return []
|
|
495
|
+
})
|
|
313
496
|
}
|
|
314
497
|
|
|
315
498
|
/**
|
|
@@ -319,9 +502,11 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
319
502
|
* @returns {FrontendModelResourceAttributePayload | Promise<FrontendModelResourceAttributePayload>} - Normalized attributes.
|
|
320
503
|
*/
|
|
321
504
|
normalizeCreateAttributes(attributes, options) {
|
|
322
|
-
|
|
505
|
+
return this.sharedResourceMethodOr("normalizeCreateAttributes", [attributes, options], () => {
|
|
506
|
+
void options
|
|
323
507
|
|
|
324
|
-
|
|
508
|
+
return attributes
|
|
509
|
+
})
|
|
325
510
|
}
|
|
326
511
|
|
|
327
512
|
/**
|
|
@@ -332,10 +517,12 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
332
517
|
* @returns {FrontendModelResourceAttributePayload | Promise<FrontendModelResourceAttributePayload>} - Normalized attributes.
|
|
333
518
|
*/
|
|
334
519
|
normalizeUpdateAttributes(model, attributes, options) {
|
|
335
|
-
|
|
336
|
-
|
|
520
|
+
return this.sharedResourceMethodOr("normalizeUpdateAttributes", [model, attributes, options], () => {
|
|
521
|
+
void model
|
|
522
|
+
void options
|
|
337
523
|
|
|
338
|
-
|
|
524
|
+
return attributes
|
|
525
|
+
})
|
|
339
526
|
}
|
|
340
527
|
|
|
341
528
|
/**
|
|
@@ -346,9 +533,11 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
346
533
|
* @returns {void | Promise<void>} - Resolves when the hook finishes.
|
|
347
534
|
*/
|
|
348
535
|
beforeCreate(model, attributes, options) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
536
|
+
return this.sharedResourceMethodOr("beforeCreate", [model, attributes, options], () => {
|
|
537
|
+
void model
|
|
538
|
+
void attributes
|
|
539
|
+
void options
|
|
540
|
+
})
|
|
352
541
|
}
|
|
353
542
|
|
|
354
543
|
/**
|
|
@@ -359,9 +548,11 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
359
548
|
* @returns {void | Promise<void>} - Resolves when the hook finishes.
|
|
360
549
|
*/
|
|
361
550
|
afterCreate(model, attributes, options) {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
551
|
+
return this.sharedResourceMethodOr("afterCreate", [model, attributes, options], () => {
|
|
552
|
+
void model
|
|
553
|
+
void attributes
|
|
554
|
+
void options
|
|
555
|
+
})
|
|
365
556
|
}
|
|
366
557
|
|
|
367
558
|
/**
|
|
@@ -372,9 +563,11 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
372
563
|
* @returns {void | Promise<void>} - Resolves when the hook finishes.
|
|
373
564
|
*/
|
|
374
565
|
beforeUpdate(model, attributes, options) {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
566
|
+
return this.sharedResourceMethodOr("beforeUpdate", [model, attributes, options], () => {
|
|
567
|
+
void model
|
|
568
|
+
void attributes
|
|
569
|
+
void options
|
|
570
|
+
})
|
|
378
571
|
}
|
|
379
572
|
|
|
380
573
|
/**
|
|
@@ -385,9 +578,11 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
385
578
|
* @returns {void | Promise<void>} - Resolves when the hook finishes.
|
|
386
579
|
*/
|
|
387
580
|
afterUpdate(model, attributes, options) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
581
|
+
return this.sharedResourceMethodOr("afterUpdate", [model, attributes, options], () => {
|
|
582
|
+
void model
|
|
583
|
+
void attributes
|
|
584
|
+
void options
|
|
585
|
+
})
|
|
391
586
|
}
|
|
392
587
|
|
|
393
588
|
/**
|
|
@@ -396,7 +591,9 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
396
591
|
* @returns {void | Promise<void>} - Resolves when the hook finishes.
|
|
397
592
|
*/
|
|
398
593
|
beforeDestroy(model) {
|
|
399
|
-
|
|
594
|
+
return this.sharedResourceMethodOr("beforeDestroy", [model], () => {
|
|
595
|
+
void model
|
|
596
|
+
})
|
|
400
597
|
}
|
|
401
598
|
|
|
402
599
|
/**
|
|
@@ -405,7 +602,9 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
405
602
|
* @returns {void | Promise<void>} - Resolves when the hook finishes.
|
|
406
603
|
*/
|
|
407
604
|
afterDestroy(model) {
|
|
408
|
-
|
|
605
|
+
return this.sharedResourceMethodOr("afterDestroy", [model], () => {
|
|
606
|
+
void model
|
|
607
|
+
})
|
|
409
608
|
}
|
|
410
609
|
|
|
411
610
|
/**
|
|
@@ -418,10 +617,12 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
418
617
|
* @returns {Promise<Result>} - Callback result.
|
|
419
618
|
*/
|
|
420
619
|
async runMutationTransaction({action, model, callback}) {
|
|
421
|
-
|
|
422
|
-
|
|
620
|
+
return await this.sharedResourceMethodOr("runMutationTransaction", [{action, model, callback}], async () => {
|
|
621
|
+
void action
|
|
622
|
+
void model
|
|
423
623
|
|
|
424
|
-
|
|
624
|
+
return await callback()
|
|
625
|
+
})
|
|
425
626
|
}
|
|
426
627
|
|
|
427
628
|
/**
|
|
@@ -517,9 +718,11 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
517
718
|
* @returns {boolean | void | Promise<boolean | void>} - Continue processing unless false.
|
|
518
719
|
*/
|
|
519
720
|
beforeAction(action) {
|
|
520
|
-
|
|
721
|
+
return this.sharedResourceMethodOr("beforeAction", [action], () => {
|
|
722
|
+
void action
|
|
521
723
|
|
|
522
|
-
|
|
724
|
+
// No-op by default.
|
|
725
|
+
})
|
|
523
726
|
}
|
|
524
727
|
|
|
525
728
|
/**
|
|
@@ -663,14 +866,14 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
|
|
|
663
866
|
/** @type {Record<string, ?>} */
|
|
664
867
|
const directAttributes = {}
|
|
665
868
|
const ResourceClass = /** @type {typeof FrontendModelBaseResource} */ (this.constructor)
|
|
666
|
-
const translatedSet = new Set(ResourceClass.
|
|
869
|
+
const translatedSet = new Set(ResourceClass.translatedAttributesConfig() || [])
|
|
667
870
|
|
|
668
871
|
for (const [name, value] of Object.entries(attributes)) {
|
|
669
872
|
const resourceSetterName = `set${inflection.camelize(name)}Attribute`
|
|
670
|
-
const resourceSetter =
|
|
873
|
+
const resourceSetter = this.resourceMethod(resourceSetterName)
|
|
671
874
|
|
|
672
|
-
if (
|
|
673
|
-
await resourceSetter.call(
|
|
875
|
+
if (resourceSetter) {
|
|
876
|
+
await resourceSetter.method.call(resourceSetter.resource, model, value)
|
|
674
877
|
} else if (translatedSet.has(name)) {
|
|
675
878
|
await this._setTranslatedAttributeOnModel(model, name, value)
|
|
676
879
|
} else {
|
|
@@ -1527,12 +1730,21 @@ function parsePermittedParams(permitSpec) {
|
|
|
1527
1730
|
}
|
|
1528
1731
|
|
|
1529
1732
|
/**
|
|
1530
|
-
*
|
|
1531
|
-
* @param {
|
|
1532
|
-
* @
|
|
1733
|
+
* Locates which prototype owns a method implementation.
|
|
1734
|
+
* @param {object} instance - Instance receiving the method.
|
|
1735
|
+
* @param {string} methodName - Method name.
|
|
1736
|
+
* @returns {object | null} - Prototype that owns the method.
|
|
1533
1737
|
*/
|
|
1534
|
-
function
|
|
1535
|
-
|
|
1738
|
+
function prototypeOwnerForMethod(instance, methodName) {
|
|
1739
|
+
let prototype = Object.getPrototypeOf(instance)
|
|
1740
|
+
|
|
1741
|
+
while (prototype) {
|
|
1742
|
+
if (Object.prototype.hasOwnProperty.call(prototype, methodName)) return prototype
|
|
1743
|
+
|
|
1744
|
+
prototype = Object.getPrototypeOf(prototype)
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
return null
|
|
1536
1748
|
}
|
|
1537
1749
|
|
|
1538
1750
|
/**
|
|
@@ -1567,7 +1779,7 @@ function filterWritableFrontendModelAttributes(
|
|
|
1567
1779
|
if (resource) {
|
|
1568
1780
|
const ResourceClass = /** @type {typeof FrontendModelBaseResource} */ (resource.constructor)
|
|
1569
1781
|
|
|
1570
|
-
translatedAttributes = ResourceClass.
|
|
1782
|
+
translatedAttributes = ResourceClass.translatedAttributesConfig() || []
|
|
1571
1783
|
}
|
|
1572
1784
|
|
|
1573
1785
|
const translatedSet = new Set(translatedAttributes)
|
|
@@ -1582,11 +1794,11 @@ function filterWritableFrontendModelAttributes(
|
|
|
1582
1794
|
const requestedSetterName = `set${inflection.camelize(resolvedAttributeName)}`
|
|
1583
1795
|
const setterName = receiverClass.findMemberNameInsensitive(receiver, requestedSetterName) || requestedSetterName
|
|
1584
1796
|
const resourceSetterName = `set${inflection.camelize(attributeName)}Attribute`
|
|
1585
|
-
const resourceSetter = resource
|
|
1797
|
+
const resourceSetter = resource?.resourceMethod(resourceSetterName)
|
|
1586
1798
|
|
|
1587
1799
|
if (setterName in receiver) {
|
|
1588
1800
|
writableAttributes[attributeName] = value
|
|
1589
|
-
} else if (
|
|
1801
|
+
} else if (resourceSetter) {
|
|
1590
1802
|
writableAttributes[attributeName] = value
|
|
1591
1803
|
} else if (translatedSet.has(attributeName)) {
|
|
1592
1804
|
writableAttributes[attributeName] = value
|
|
@@ -26,9 +26,6 @@ export default class VelociousAttachmentResource extends FrontendModelBaseResour
|
|
|
26
26
|
updatedAt: {type: "datetime"}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/** @type {string[]} */
|
|
30
|
-
static abilities = ["read"]
|
|
31
|
-
|
|
32
29
|
/** @type {string[]} */
|
|
33
30
|
static builtInCollectionCommands = ["index"]
|
|
34
31
|
|
|
@@ -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
|
|