velocious 1.0.489 → 1.0.492

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.
Files changed (103) hide show
  1. package/build/application.js +3 -0
  2. package/build/cli/commands/db/tenants/create.js +7 -5
  3. package/build/cli/commands/db/tenants/drop.js +6 -6
  4. package/build/configuration-types.js +8 -0
  5. package/build/configuration.js +29 -0
  6. package/build/database/query/index.js +30 -0
  7. package/build/database/query/model-class-query.js +17 -0
  8. package/build/database/record/index.js +16 -0
  9. package/build/src/application.d.ts.map +1 -1
  10. package/build/src/application.js +3 -1
  11. package/build/src/cli/commands/db/tenants/create.d.ts.map +1 -1
  12. package/build/src/cli/commands/db/tenants/create.js +8 -5
  13. package/build/src/cli/commands/db/tenants/drop.d.ts +1 -1
  14. package/build/src/cli/commands/db/tenants/drop.d.ts.map +1 -1
  15. package/build/src/cli/commands/db/tenants/drop.js +7 -6
  16. package/build/src/configuration-types.d.ts +24 -0
  17. package/build/src/configuration-types.d.ts.map +1 -1
  18. package/build/src/configuration-types.js +8 -1
  19. package/build/src/configuration.d.ts +6 -0
  20. package/build/src/configuration.d.ts.map +1 -1
  21. package/build/src/configuration.js +26 -1
  22. package/build/src/database/query/index.d.ts +25 -0
  23. package/build/src/database/query/index.d.ts.map +1 -1
  24. package/build/src/database/query/index.js +26 -1
  25. package/build/src/database/query/model-class-query.d.ts +10 -0
  26. package/build/src/database/query/model-class-query.d.ts.map +1 -1
  27. package/build/src/database/query/model-class-query.js +16 -1
  28. package/build/src/database/record/index.d.ts +7 -0
  29. package/build/src/database/record/index.d.ts.map +1 -1
  30. package/build/src/database/record/index.js +15 -1
  31. package/build/src/sync/query-scope.d.ts +18 -0
  32. package/build/src/sync/query-scope.d.ts.map +1 -0
  33. package/build/src/sync/query-scope.js +77 -0
  34. package/build/src/sync/server-change-feed.d.ts.map +1 -1
  35. package/build/src/sync/server-change-feed.js +2 -24
  36. package/build/src/sync/stable-json.d.ts +9 -0
  37. package/build/src/sync/stable-json.d.ts.map +1 -0
  38. package/build/src/sync/stable-json.js +27 -0
  39. package/build/src/sync/sync-api-client.d.ts +185 -5
  40. package/build/src/sync/sync-api-client.d.ts.map +1 -1
  41. package/build/src/sync/sync-api-client.js +309 -17
  42. package/build/src/sync/sync-api-controller.d.ts +8 -0
  43. package/build/src/sync/sync-api-controller.d.ts.map +1 -1
  44. package/build/src/sync/sync-api-controller.js +17 -1
  45. package/build/src/sync/sync-client-registry.d.ts +12 -0
  46. package/build/src/sync/sync-client-registry.d.ts.map +1 -0
  47. package/build/src/sync/sync-client-registry.js +27 -0
  48. package/build/src/sync/sync-client-types.d.ts +127 -0
  49. package/build/src/sync/sync-client-types.d.ts.map +1 -0
  50. package/build/src/sync/sync-client-types.js +3 -0
  51. package/build/src/sync/sync-client.d.ts +176 -0
  52. package/build/src/sync/sync-client.d.ts.map +1 -0
  53. package/build/src/sync/sync-client.js +380 -0
  54. package/build/src/sync/sync-envelope-replay-service.d.ts +38 -4
  55. package/build/src/sync/sync-envelope-replay-service.d.ts.map +1 -1
  56. package/build/src/sync/sync-envelope-replay-service.js +73 -6
  57. package/build/src/sync/sync-resource-base.d.ts +98 -0
  58. package/build/src/sync/sync-resource-base.d.ts.map +1 -0
  59. package/build/src/sync/sync-resource-base.js +122 -0
  60. package/build/src/sync/sync-scope-store.d.ts +128 -0
  61. package/build/src/sync/sync-scope-store.d.ts.map +1 -0
  62. package/build/src/sync/sync-scope-store.js +282 -0
  63. package/build/src/tenants/default-tenant-database-provisioning.d.ts +21 -0
  64. package/build/src/tenants/default-tenant-database-provisioning.d.ts.map +1 -0
  65. package/build/src/tenants/default-tenant-database-provisioning.js +106 -0
  66. package/build/src/tenants/tenant.d.ts.map +1 -1
  67. package/build/src/tenants/tenant.js +6 -5
  68. package/build/sync/query-scope.js +84 -0
  69. package/build/sync/server-change-feed.js +1 -25
  70. package/build/sync/stable-json.js +28 -0
  71. package/build/sync/sync-api-client.js +331 -17
  72. package/build/sync/sync-api-controller.js +20 -0
  73. package/build/sync/sync-client-registry.js +30 -0
  74. package/build/sync/sync-client-types.js +41 -0
  75. package/build/sync/sync-client.js +433 -0
  76. package/build/sync/sync-envelope-replay-service.js +77 -5
  77. package/build/sync/sync-resource-base.js +143 -0
  78. package/build/sync/sync-scope-store.js +327 -0
  79. package/build/tenants/default-tenant-database-provisioning.js +124 -0
  80. package/build/tenants/tenant.js +5 -5
  81. package/build/tsconfig.tsbuildinfo +1 -1
  82. package/package.json +1 -1
  83. package/src/application.js +3 -0
  84. package/src/cli/commands/db/tenants/create.js +7 -5
  85. package/src/cli/commands/db/tenants/drop.js +6 -6
  86. package/src/configuration-types.js +8 -0
  87. package/src/configuration.js +29 -0
  88. package/src/database/query/index.js +30 -0
  89. package/src/database/query/model-class-query.js +17 -0
  90. package/src/database/record/index.js +16 -0
  91. package/src/sync/query-scope.js +84 -0
  92. package/src/sync/server-change-feed.js +1 -25
  93. package/src/sync/stable-json.js +28 -0
  94. package/src/sync/sync-api-client.js +331 -17
  95. package/src/sync/sync-api-controller.js +20 -0
  96. package/src/sync/sync-client-registry.js +30 -0
  97. package/src/sync/sync-client-types.js +41 -0
  98. package/src/sync/sync-client.js +433 -0
  99. package/src/sync/sync-envelope-replay-service.js +77 -5
  100. package/src/sync/sync-resource-base.js +143 -0
  101. package/src/sync/sync-scope-store.js +327 -0
  102. package/src/tenants/default-tenant-database-provisioning.js +124 -0
  103. package/src/tenants/tenant.js +5 -5
@@ -1,6 +1,7 @@
1
1
  // @ts-check
2
2
 
3
3
  import BaseCommand from "../../../base-command.js"
4
+ import {createTenantDatabase} from "../../../../tenants/default-tenant-database-provisioning.js"
4
5
  import TenantDatabaseCommandHelper from "../../../tenant-database-command-helper.js"
5
6
 
6
7
  export default class DbTenantsCreate extends BaseCommand {
@@ -14,13 +15,14 @@ export default class DbTenantsCreate extends BaseCommand {
14
15
  identifier: this.processArgs?.[1]
15
16
  })
16
17
  const provider = helper.provider
17
-
18
- if (typeof provider.createDatabase !== "function") {
19
- throw new Error(`Tenant database provider for ${helper.identifier} must define createDatabase to use db:tenants:create`)
20
- }
18
+ // Providers may customize creation, but the generic driver-agnostic provisioning
19
+ // is the framework default so apps do not reimplement it.
20
+ const createDatabase = typeof provider.createDatabase === "function"
21
+ ? provider.createDatabase.bind(provider)
22
+ : createTenantDatabase
21
23
 
22
24
  const tenantCount = await helper.eachTenant(async ({databaseConfiguration, tenant}) => {
23
- await provider.createDatabase?.({
25
+ await createDatabase({
24
26
  configuration: this.getConfiguration(),
25
27
  databaseConfiguration,
26
28
  identifier: helper.identifier,
@@ -1,12 +1,13 @@
1
1
  // @ts-check
2
2
 
3
3
  import BaseCommand from "../../../base-command.js"
4
+ import {dropTenantDatabase} from "../../../../tenants/default-tenant-database-provisioning.js"
4
5
  import TenantDatabaseCommandHelper from "../../../tenant-database-command-helper.js"
5
6
 
6
7
  export default class DbTenantsDrop extends BaseCommand {
7
8
  /**
8
9
  * Drops the tenant database/schema for every listed tenant through the provider's
9
- * `dropDatabase` hook.
10
+ * `dropDatabase` hook, or the framework default when the provider defines none.
10
11
  * @returns {Promise<{identifier: string, tenantCount: number} | void>} - Result in test mode.
11
12
  */
12
13
  async execute() {
@@ -15,13 +16,12 @@ export default class DbTenantsDrop extends BaseCommand {
15
16
  identifier: this.processArgs?.[1]
16
17
  })
17
18
  const provider = helper.provider
18
-
19
- if (typeof provider.dropDatabase !== "function") {
20
- throw new Error(`Tenant database provider for ${helper.identifier} must define dropDatabase to use db:tenants:drop`)
21
- }
19
+ const dropDatabase = typeof provider.dropDatabase === "function"
20
+ ? provider.dropDatabase.bind(provider)
21
+ : dropTenantDatabase
22
22
 
23
23
  const tenantCount = await helper.eachTenant(async ({databaseConfiguration, tenant}) => {
24
- await provider.dropDatabase?.({
24
+ await dropDatabase({
25
25
  configuration: this.getConfiguration(),
26
26
  databaseConfiguration,
27
27
  identifier: helper.identifier,
@@ -355,9 +355,17 @@
355
355
  * @property {Record<string, FrontendModelSyncJsonValue>} [policy] - Deterministic non-secret policy inputs included in the policy hash only.
356
356
  */
357
357
 
358
+ /**
359
+ * Velocious sync API endpoint configuration.
360
+ * @typedef {object} VelociousSyncApiConfiguration
361
+ * @property {string} [mountPath] - Mount path for the sync endpoints. Defaults to "/velocious/sync".
362
+ * @property {FrontendModelResourceClassType} resourceClass - App sync resource class served by the auto-mounted sync endpoints.
363
+ */
364
+
358
365
  /**
359
366
  * Velocious sync configuration.
360
367
  * @typedef {object} VelociousSyncConfiguration
368
+ * @property {VelociousSyncApiConfiguration} [api] - Auto-mounts the Velocious sync changes/replay endpoints for this resource class.
361
369
  * @property {import("./sync/device-identity.js").SyncJsonWebKey | null} [deviceCertificateBackendPublicKey] - Public backend key used to verify offline device certificates for sync replay.
362
370
  * @property {number} [changeFeedRetentionSize] - Number of accepted server changes retained before clients must refresh from snapshot.
363
371
  * @property {Array<import("./sync/offline-grant.js").OfflineGrantSigningKey>} offlineGrantSigningKeys - Signing keys used to issue and verify offline grants. Secrets must never be exposed to clients.
@@ -424,6 +424,7 @@ export default class VelociousConfiguration {
424
424
  * @returns {import("./configuration-types.js").VelociousSyncConfiguration} - Normalized sync configuration.
425
425
  */
426
426
  _normalizeSyncConfiguration(sync) {
427
+ const api = sync?.api
427
428
  const deviceCertificateBackendPublicKey = sync?.deviceCertificateBackendPublicKey || null
428
429
  const changeFeedRetentionSize = sync?.changeFeedRetentionSize
429
430
  const offlineGrantSigningKeys = sync?.offlineGrantSigningKeys || []
@@ -441,6 +442,7 @@ export default class VelociousConfiguration {
441
442
  }
442
443
 
443
444
  return {
445
+ api: this._normalizeSyncApiConfiguration(api),
444
446
  changeFeedRetentionSize: changeFeedRetentionSize || 10000,
445
447
  deviceCertificateBackendPublicKey,
446
448
  offlineGrantSigningKeys: offlineGrantSigningKeys.map((key) => normalizeOfflineGrantSigningKey(key)),
@@ -448,6 +450,33 @@ export default class VelociousConfiguration {
448
450
  }
449
451
  }
450
452
 
453
+ /**
454
+ * Normalizes sync API endpoint configuration.
455
+ * @param {import("./configuration-types.js").VelociousSyncApiConfiguration | undefined} api - Sync API configuration.
456
+ * @returns {import("./configuration-types.js").VelociousSyncApiConfiguration | undefined} - Normalized sync API configuration.
457
+ */
458
+ _normalizeSyncApiConfiguration(api) {
459
+ if (api === undefined || api === null) return undefined
460
+
461
+ if (typeof api !== "object" || Array.isArray(api)) {
462
+ throw new Error("sync.api must be an object with a resourceClass")
463
+ }
464
+
465
+ const {mountPath, resourceClass} = api
466
+
467
+ if (typeof resourceClass !== "function") {
468
+ throw new Error(`sync.api.resourceClass must be a resource class, got: ${String(resourceClass)}`)
469
+ }
470
+ if (!resourceClass.ModelClass) {
471
+ throw new Error(`sync.api.resourceClass ${resourceClass.name} must define static ModelClass`)
472
+ }
473
+ if (mountPath !== undefined && (typeof mountPath !== "string" || !mountPath.startsWith("/"))) {
474
+ throw new Error(`sync.api.mountPath must start with '/', got: ${String(mountPath)}`)
475
+ }
476
+
477
+ return {mountPath, resourceClass}
478
+ }
479
+
451
480
  /**
452
481
  * Runs get database configuration.
453
482
  * @returns {Record<string, import("./configuration-types.js").DatabaseConfigurationType>} - The database configuration.
@@ -235,18 +235,48 @@ export default class VelociousDatabaseQuery {
235
235
  return this._groups
236
236
  }
237
237
 
238
+ /**
239
+ * Runs get joins.
240
+ * @returns {Array<?>} - The joins.
241
+ */
242
+ getJoins() { return this._joins }
243
+
244
+ /**
245
+ * Runs get limit.
246
+ * @returns {number | null} - The limit.
247
+ */
248
+ getLimit() { return this._limit }
249
+
250
+ /**
251
+ * Runs get offset.
252
+ * @returns {number | null} - The offset.
253
+ */
254
+ getOffset() { return this._offset }
255
+
238
256
  /**
239
257
  * Runs get options.
240
258
  * @returns {import("../query-parser/options.js").default} - The options options.
241
259
  */
242
260
  getOptions() { return this.driver.options() }
243
261
 
262
+ /**
263
+ * Runs get orders.
264
+ * @returns {Array<?>} - The orders.
265
+ */
266
+ getOrders() { return this._orders }
267
+
244
268
  /**
245
269
  * Runs get selects.
246
270
  * @returns {Array<import("./select-base.js").default>} - The selects.
247
271
  */
248
272
  getSelects() { return this._selects }
249
273
 
274
+ /**
275
+ * Runs get wheres.
276
+ * @returns {Array<import("./where-base.js").default>} - The wheres.
277
+ */
278
+ getWheres() { return this._wheres }
279
+
250
280
  /**
251
281
  * Runs from.
252
282
  * @param {string|import("./from-base.js").default} from - From.
@@ -3,6 +3,7 @@
3
3
  import {incorporate} from "incorporator"
4
4
  import * as inflection from "inflection"
5
5
  import {isPlainObject} from "is-plain-object"
6
+ import {currentSyncClient} from "../../sync/sync-client-registry.js"
6
7
  import Logger from "../../logger.js"
7
8
  import Preloader from "./preloader.js"
8
9
  import {normalizeQueryDataSpec, runQueryData} from "./query-data.js"
@@ -1114,6 +1115,22 @@ export default class VelociousDatabaseQueryModelClassQuery extends DatabaseQuery
1114
1115
  queryLogName(operation) {
1115
1116
  return `${this.getModelClass().name} ${operation}`
1116
1117
  }
1118
+
1119
+ /**
1120
+ * Declares this query as a sync scope on the current sync client.
1121
+ * @returns {Promise<?>} - Declared scope and pull result.
1122
+ */
1123
+ async sync() {
1124
+ return await currentSyncClient().sync(this)
1125
+ }
1126
+
1127
+ /**
1128
+ * Deactivates this query's sync scope on the current sync client.
1129
+ * @returns {Promise<void>} - Resolves when the scope is deactivated.
1130
+ */
1131
+ async unsync() {
1132
+ await currentSyncClient().unsync(this)
1133
+ }
1117
1134
  }
1118
1135
 
1119
1136
  /**
@@ -535,6 +535,22 @@ class VelociousDatabaseRecord {
535
535
  callbacks[callbackName].push(callback)
536
536
  }
537
537
 
538
+ /**
539
+ * Runs unregister lifecycle callback.
540
+ * @param {"afterCreate" | "afterDestroy" | "afterSave" | "afterUpdate" | "beforeCreate" | "beforeDestroy" | "beforeSave" | "beforeUpdate" | "beforeValidation"} callbackName - Callback type.
541
+ * @param {LifecycleCallbackType} callback - Previously registered callback.
542
+ * @returns {void}
543
+ */
544
+ static unregisterLifecycleCallback(callbackName, callback) {
545
+ const callbacks = this.getLifecycleCallbacksMap()[callbackName]
546
+
547
+ if (!callbacks) return
548
+
549
+ const callbackIndex = callbacks.indexOf(callback)
550
+
551
+ if (callbackIndex >= 0) callbacks.splice(callbackIndex, 1)
552
+ }
553
+
538
554
  /**
539
555
  * Runs before validation.
540
556
  * @template R
@@ -0,0 +1,84 @@
1
+ // @ts-check
2
+
3
+ import stableJsonStringify from "./stable-json.js"
4
+
5
+ /**
6
+ * Serializes a model query into a transportable sync scope.
7
+ *
8
+ * Only plain attribute equality conditions are supported: the scope must be
9
+ * expressible as `{resourceType, conditions}` so servers can match it against
10
+ * their change feeds. Anything else (raw SQL, negations, joins, orders,
11
+ * limits, offsets, groups) fails loudly.
12
+ * @param {import("../database/query/model-class-query.js").default<?>} query - Model query declaring the sync scope.
13
+ * @returns {import("./sync-client-types.js").SerializedSyncScope} Serialized sync scope.
14
+ */
15
+ export function serializedScopeFromQuery(query) {
16
+ if (query.getJoins().length > 0) throw new Error("sync(query) does not support joins")
17
+ if (query.getOrders().length > 0) throw new Error("sync(query) does not support orders")
18
+ if (query.getLimit() !== null && query.getLimit() !== undefined) throw new Error("sync(query) does not support limit")
19
+ if (query.getOffset() !== null && query.getOffset() !== undefined) throw new Error("sync(query) does not support offset")
20
+ if (query.getGroups().length > 0) throw new Error("sync(query) does not support groups")
21
+
22
+ const modelClass = query.getModelClass()
23
+ const conditions = /** @type {Record<string, ?>} */ ({})
24
+
25
+ for (const where of query.getWheres()) {
26
+ const whereHash = /** @type {{hash?: Record<string, ?>}} */ (where).hash
27
+
28
+ if (!whereHash || typeof whereHash !== "object" || Array.isArray(whereHash) || /** @type {{where?: ?}} */ (where).where) {
29
+ throw new Error(`sync(query) only supports plain attribute conditions, got: ${where.constructor.name}`)
30
+ }
31
+
32
+ for (const [attributeName, value] of Object.entries(whereHash)) {
33
+ const conditionValue = scalarConditionValue(attributeName, value)
34
+
35
+ if (attributeName in conditions && stableJsonStringify(conditions[attributeName]) !== stableJsonStringify(conditionValue)) {
36
+ throw new Error(`sync(query) got conflicting conditions for: ${attributeName}`)
37
+ }
38
+
39
+ conditions[attributeName] = conditionValue
40
+ }
41
+ }
42
+
43
+ return {conditions, resourceType: modelClass.getModelName()}
44
+ }
45
+
46
+ /**
47
+ * Returns a stable canonical key identifying a sync scope.
48
+ * @param {import("./sync-client-types.js").SerializedSyncScope} scope - Serialized sync scope.
49
+ * @returns {string} Stable scope key.
50
+ */
51
+ export function scopeKey(scope) {
52
+ return `${scope.resourceType}:${stableJsonStringify(scope.conditions)}`
53
+ }
54
+
55
+ /**
56
+ * Validates one scope condition value as a scalar or array of scalars.
57
+ * @param {string} attributeName - Condition attribute name for error messages.
58
+ * @param {?} value - Condition value.
59
+ * @returns {?} Validated condition value.
60
+ */
61
+ function scalarConditionValue(attributeName, value) {
62
+ if (Array.isArray(value)) {
63
+ for (const item of value) validateScalar(attributeName, item)
64
+
65
+ return value
66
+ }
67
+
68
+ validateScalar(attributeName, value)
69
+
70
+ return value
71
+ }
72
+
73
+ /**
74
+ * Validates one scalar condition value.
75
+ * @param {string} attributeName - Condition attribute name for error messages.
76
+ * @param {?} value - Condition value.
77
+ * @returns {void}
78
+ */
79
+ function validateScalar(attributeName, value) {
80
+ if (value === null) return
81
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return
82
+
83
+ throw new Error(`sync(query) condition values must be scalar, got ${typeof value} for: ${attributeName}`)
84
+ }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import {randomUUID} from "crypto"
4
4
  import TableData from "../database/table-data/index.js"
5
+ import stableJsonStringify from "./stable-json.js"
5
6
 
6
7
  const DEFAULT_RETENTION_SIZE = 10000
7
8
  const DEFAULT_PAGE_SIZE = 100
@@ -464,31 +465,6 @@ function scopesEqual(changeScope, requestedScope) {
464
465
  return stableJsonStringify(changeScope || null) === stableJsonStringify(requestedScope)
465
466
  }
466
467
 
467
- /**
468
- * Stable JSON serialization for persisted sync scopes.
469
- * @param {?} value - JSON-compatible value.
470
- * @returns {string} - Stable JSON representation.
471
- */
472
- function stableJsonStringify(value) {
473
- return JSON.stringify(stableJsonValue(value))
474
- }
475
-
476
- /**
477
- * Produces a recursively key-sorted JSON value.
478
- * @param {?} value - JSON-compatible value.
479
- * @returns {?} - Stable JSON-compatible value.
480
- */
481
- function stableJsonValue(value) {
482
- if (Array.isArray(value)) return value.map((item) => stableJsonValue(item))
483
- if (!value || typeof value !== "object") return value
484
-
485
- return Object.keys(value).sort().reduce((memo, key) => {
486
- memo[key] = stableJsonValue(value[key])
487
-
488
- return memo
489
- }, /** @type {Record<string, ?>} */ ({}))
490
- }
491
-
492
468
  /**
493
469
  * @typedef {object} ServerChangeFeedEntry
494
470
  * @property {string | null} actorDeviceId - Signed mutation actor device id when available.
@@ -0,0 +1,28 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * Serializes a JSON-compatible value with recursively sorted object keys, so
5
+ * equal values always produce byte-identical strings (used for sync scope and
6
+ * change-feed identity comparisons).
7
+ * @param {?} value - JSON-compatible value.
8
+ * @returns {string} - Stable JSON string.
9
+ */
10
+ export default function stableJsonStringify(value) {
11
+ return JSON.stringify(stableJsonValue(value))
12
+ }
13
+
14
+ /**
15
+ * Produces a recursively key-sorted JSON value.
16
+ * @param {?} value - JSON-compatible value.
17
+ * @returns {?} - Stable JSON-compatible value.
18
+ */
19
+ function stableJsonValue(value) {
20
+ if (Array.isArray(value)) return value.map((item) => stableJsonValue(item))
21
+ if (!value || typeof value !== "object") return value
22
+
23
+ return Object.keys(value).sort().reduce((memo, key) => {
24
+ memo[key] = stableJsonValue(value[key])
25
+
26
+ return memo
27
+ }, /** @type {Record<string, ?>} */ ({}))
28
+ }