velocious 1.0.441 → 1.0.443

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 (88) hide show
  1. package/build/authorization/base-resource.js +2 -2
  2. package/build/beacon/client.js +13 -7
  3. package/build/beacon/server.js +11 -0
  4. package/build/cli/commands/lint/relationships.js +12 -0
  5. package/build/configuration-types.js +5 -1
  6. package/build/controller.js +1 -1
  7. package/build/database/drivers/base.js +4 -1
  8. package/build/database/record/index.js +46 -34
  9. package/build/database/record/relationships/belongs-to.js +1 -1
  10. package/build/database/record/relationships/has-many.js +3 -1
  11. package/build/database/record/relationships/has-one.js +3 -1
  12. package/build/environment-handlers/base.js +10 -0
  13. package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +2 -2
  14. package/build/environment-handlers/node/cli/commands/lint/relationships.js +144 -0
  15. package/build/environment-handlers/node.js +10 -0
  16. package/build/frontend-model-resource/base-resource.js +9 -10
  17. package/build/frontend-models/base.js +6 -6
  18. package/build/frontend-models/query.js +2 -2
  19. package/build/src/authorization/base-resource.d.ts +4 -4
  20. package/build/src/authorization/base-resource.d.ts.map +1 -1
  21. package/build/src/authorization/base-resource.js +3 -3
  22. package/build/src/beacon/client.d.ts.map +1 -1
  23. package/build/src/beacon/client.js +13 -8
  24. package/build/src/beacon/server.d.ts +5 -0
  25. package/build/src/beacon/server.d.ts.map +1 -1
  26. package/build/src/beacon/server.js +11 -1
  27. package/build/src/cli/commands/lint/relationships.d.ts +5 -0
  28. package/build/src/cli/commands/lint/relationships.d.ts.map +1 -0
  29. package/build/src/cli/commands/lint/relationships.js +12 -0
  30. package/build/src/configuration-types.d.ts +7 -3
  31. package/build/src/configuration-types.d.ts.map +1 -1
  32. package/build/src/configuration-types.js +5 -2
  33. package/build/src/controller.d.ts +3 -3
  34. package/build/src/controller.d.ts.map +1 -1
  35. package/build/src/controller.js +2 -2
  36. package/build/src/database/drivers/base.d.ts.map +1 -1
  37. package/build/src/database/drivers/base.js +5 -2
  38. package/build/src/database/record/index.d.ts +43 -37
  39. package/build/src/database/record/index.d.ts.map +1 -1
  40. package/build/src/database/record/index.js +45 -35
  41. package/build/src/database/record/relationships/belongs-to.js +2 -2
  42. package/build/src/database/record/relationships/has-many.d.ts.map +1 -1
  43. package/build/src/database/record/relationships/has-many.js +3 -2
  44. package/build/src/database/record/relationships/has-one.d.ts.map +1 -1
  45. package/build/src/database/record/relationships/has-one.js +3 -2
  46. package/build/src/environment-handlers/base.d.ts +7 -0
  47. package/build/src/environment-handlers/base.d.ts.map +1 -1
  48. package/build/src/environment-handlers/base.js +10 -1
  49. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +3 -3
  50. package/build/src/environment-handlers/node/cli/commands/lint/relationships.d.ts +34 -0
  51. package/build/src/environment-handlers/node/cli/commands/lint/relationships.d.ts.map +1 -0
  52. package/build/src/environment-handlers/node/cli/commands/lint/relationships.js +123 -0
  53. package/build/src/environment-handlers/node.d.ts.map +1 -1
  54. package/build/src/environment-handlers/node.js +10 -1
  55. package/build/src/frontend-model-resource/base-resource.d.ts +15 -16
  56. package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
  57. package/build/src/frontend-model-resource/base-resource.js +10 -11
  58. package/build/src/frontend-models/base.d.ts +18 -12
  59. package/build/src/frontend-models/base.d.ts.map +1 -1
  60. package/build/src/frontend-models/base.js +7 -7
  61. package/build/src/frontend-models/query.d.ts +4 -4
  62. package/build/src/frontend-models/query.d.ts.map +1 -1
  63. package/build/src/frontend-models/query.js +3 -3
  64. package/build/src/utils/is-date.d.ts +10 -0
  65. package/build/src/utils/is-date.d.ts.map +1 -0
  66. package/build/src/utils/is-date.js +13 -0
  67. package/build/tsconfig.tsbuildinfo +1 -1
  68. package/build/utils/is-date.js +13 -0
  69. package/package.json +1 -1
  70. package/src/authorization/base-resource.js +2 -2
  71. package/src/beacon/client.js +13 -7
  72. package/src/beacon/server.js +11 -0
  73. package/src/cli/commands/lint/relationships.js +12 -0
  74. package/src/configuration-types.js +5 -1
  75. package/src/controller.js +1 -1
  76. package/src/database/drivers/base.js +4 -1
  77. package/src/database/record/index.js +46 -34
  78. package/src/database/record/relationships/belongs-to.js +1 -1
  79. package/src/database/record/relationships/has-many.js +3 -1
  80. package/src/database/record/relationships/has-one.js +3 -1
  81. package/src/environment-handlers/base.js +10 -0
  82. package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +2 -2
  83. package/src/environment-handlers/node/cli/commands/lint/relationships.js +144 -0
  84. package/src/environment-handlers/node.js +10 -0
  85. package/src/frontend-model-resource/base-resource.js +9 -10
  86. package/src/frontend-models/base.js +6 -6
  87. package/src/frontend-models/query.js +2 -2
  88. package/src/utils/is-date.js +13 -0
@@ -111,7 +111,7 @@ export default class AuthorizationBaseResource {
111
111
 
112
112
  /**
113
113
  * Runs current user.
114
- * @returns {import("../configuration-types.js").VelociousLooseObject | null | undefined} - Current user from context.
114
+ * @returns {unknown} - Current user from context.
115
115
  */
116
116
  currentUser() {
117
117
  return this.context.currentUser
@@ -127,7 +127,7 @@ export default class AuthorizationBaseResource {
127
127
 
128
128
  /**
129
129
  * Runs params.
130
- * @returns {import("../configuration-types.js").VelociousLooseObject | undefined} - Params from context.
130
+ * @returns {import("../configuration-types.js").VelociousParams | undefined} - Params from context.
131
131
  */
132
132
  params() {
133
133
  return this.context.params
@@ -225,14 +225,20 @@ export default class BeaconClient extends EventEmitter {
225
225
  this._reconnectTimer = undefined
226
226
  }
227
227
 
228
- if (this._socket) {
229
- const socket = this._socket
228
+ const socket = this._socket
230
229
 
231
- await new Promise((resolve) => {
232
- socket.once("close", () => resolve(undefined))
233
- socket.end()
234
- })
235
- }
230
+ if (!socket) return
231
+
232
+ this._socket = undefined
233
+ this._jsonSocket = undefined
234
+
235
+ if (socket.destroyed) return
236
+
237
+ await new Promise((resolve) => {
238
+ socket.once("close", () => resolve(undefined))
239
+ socket.end()
240
+ socket.destroySoon()
241
+ })
236
242
  }
237
243
 
238
244
  /**
@@ -37,6 +37,11 @@ export default class BeaconServer {
37
37
  * Narrows the runtime value to the documented type.
38
38
  @type {net.Server | undefined} */
39
39
  this.server = undefined
40
+ /**
41
+ * Accepted sockets, including connections that have not completed the hello handshake yet.
42
+ * @type {Set<net.Socket>}
43
+ */
44
+ this.sockets = new Set()
40
45
  }
41
46
 
42
47
  /**
@@ -68,6 +73,10 @@ export default class BeaconServer {
68
73
  peer.close()
69
74
  }
70
75
 
76
+ for (const socket of this.sockets) {
77
+ socket.destroy()
78
+ }
79
+
71
80
  if (!this.server) return
72
81
 
73
82
  const {server} = this
@@ -97,6 +106,7 @@ export default class BeaconServer {
97
106
  * @returns {void}
98
107
  */
99
108
  _handleConnection(socket) {
109
+ this.sockets.add(socket)
100
110
  const jsonSocket = new JsonSocket(socket)
101
111
  /**
102
112
  * Defines peerId.
@@ -104,6 +114,7 @@ export default class BeaconServer {
104
114
  let peerId
105
115
 
106
116
  const cleanup = () => {
117
+ this.sockets.delete(socket)
107
118
  this.peers.delete(jsonSocket)
108
119
  }
109
120
 
@@ -0,0 +1,12 @@
1
+ import BaseCommand from "../../base-command.js"
2
+
3
+ /** Lints model relationships (e.g. belongs-to relationships missing an inverse on the target model). */
4
+ export default class VelociousCliCommandsLintRelationships extends BaseCommand {
5
+ /**
6
+ * Runs execute.
7
+ * @returns {Promise<?>} - Resolves with the command result.
8
+ */
9
+ async execute() {
10
+ return await this.getConfiguration().getEnvironmentHandler().cliCommandsLintRelationships(this)
11
+ }
12
+ }
@@ -202,7 +202,11 @@
202
202
  */
203
203
 
204
204
  /**
205
- * @typedef {Record<string, unknown> & {configuration?: import("./configuration.js").default, currentUser?: Record<string, unknown> | null, params?: Record<string, unknown>, request?: import("./http-server/client/request.js").default | import("./http-server/client/websocket-request.js").default}} VelociousLooseObject
205
+ * @typedef {Record<string, string>} VelociousParams
206
+ */
207
+
208
+ /**
209
+ * @typedef {Record<string, unknown> & {configuration?: import("./configuration.js").default, currentUser?: unknown, params?: VelociousParams, request?: import("./http-server/client/request.js").default | import("./http-server/client/websocket-request.js").default}} VelociousLooseObject
206
210
  */
207
211
 
208
212
  /**
@@ -34,7 +34,7 @@ export default class VelociousController {
34
34
  * @param {string} args.action - Action.
35
35
  * @param {import("./configuration.js").default} args.configuration - Configuration instance.
36
36
  * @param {string} args.controller - Controller.
37
- * @param {object} args.params - Parameters object.
37
+ * @param {Record<string, ?>} args.params - Parameters object.
38
38
  * @param {import("./http-server/client/request.js").default} args.request - Request object.
39
39
  * @param {import("./http-server/client/response.js").default} args.response - Response object.
40
40
  * @param {string} args.viewPath - View path.
@@ -103,6 +103,7 @@
103
103
 
104
104
  import BacktraceCleaner from "../../utils/backtrace-cleaner.js"
105
105
  import { getDatabaseAnnotations } from "../annotations.js"
106
+ import isDate from "../../utils/is-date.js"
106
107
  import Logger from "../../logger.js"
107
108
  import Query from "../query/index.js"
108
109
  import Handler from "../handler.js"
@@ -644,7 +645,9 @@ export default class VelociousDatabaseDriversBase {
644
645
  return value ? 1 : 0
645
646
  }
646
647
 
647
- if (value instanceof Date) {
648
+ // isDate instead of instanceof: a Date created in another realm (e.g. the console REPL) would
649
+ // fail instanceof, skip this conversion, and serialize as an empty SQL value downstream.
650
+ if (isDate(value)) {
648
651
  return strftime("%F %T.%L", value)
649
652
  }
650
653
 
@@ -14,7 +14,7 @@
14
14
  /**
15
15
  * Model class constructor type used for static `this` typing.
16
16
  * @template {VelociousDatabaseRecord} T
17
- * @typedef {{new (...args: Array<never>): T}} ModelConstructor
17
+ * @typedef {{new (changes?: Record<string, unknown>): T}} ModelConstructor
18
18
  */
19
19
 
20
20
  import timeout from "awaitery/build/timeout.js"
@@ -31,6 +31,7 @@ import HasOneRelationship from "./relationships/has-one.js"
31
31
  import RecordAttachmentHandle from "./attachments/handle.js"
32
32
  import * as inflection from "inflection"
33
33
  import deburrColumnName from "../../utils/deburr-column-name.js"
34
+ import isDate from "../../utils/is-date.js"
34
35
  import ModelClassQuery from "../query/model-class-query.js"
35
36
  import Preloader from "../query/preloader.js"
36
37
  import {readPayloadAssociationCount, readPayloadComputedAbility, readPayloadQueryData, setPayloadAssociationCount, setPayloadComputedAbility, setPayloadQueryData} from "../../record-payload-values.js"
@@ -500,9 +501,9 @@ class VelociousDatabaseRecord {
500
501
 
501
502
  /**
502
503
  * Runs before validation.
503
- * @template {VelociousDatabaseRecord} T
504
- * @this {ModelConstructor<T> & typeof VelociousDatabaseRecord}
505
- * @param {LifecycleCallbackType<T>} callback - Callback function or instance method name.
504
+ * @template {typeof VelociousDatabaseRecord} MC
505
+ * @this {MC}
506
+ * @param {LifecycleCallbackType<InstanceType<MC>>} callback - Callback function or instance method name.
506
507
  * @returns {void}
507
508
  */
508
509
  static beforeValidation(callback) {
@@ -511,9 +512,9 @@ class VelociousDatabaseRecord {
511
512
 
512
513
  /**
513
514
  * Runs before save.
514
- * @template {VelociousDatabaseRecord} T
515
- * @this {ModelConstructor<T> & typeof VelociousDatabaseRecord}
516
- * @param {LifecycleCallbackType<T>} callback - Callback function or instance method name.
515
+ * @template {typeof VelociousDatabaseRecord} MC
516
+ * @this {MC}
517
+ * @param {LifecycleCallbackType<InstanceType<MC>>} callback - Callback function or instance method name.
517
518
  * @returns {void}
518
519
  */
519
520
  static beforeSave(callback) {
@@ -522,9 +523,9 @@ class VelociousDatabaseRecord {
522
523
 
523
524
  /**
524
525
  * Runs before create.
525
- * @template {VelociousDatabaseRecord} T
526
- * @this {ModelConstructor<T> & typeof VelociousDatabaseRecord}
527
- * @param {LifecycleCallbackType<T>} callback - Callback function or instance method name.
526
+ * @template {typeof VelociousDatabaseRecord} MC
527
+ * @this {MC}
528
+ * @param {LifecycleCallbackType<InstanceType<MC>>} callback - Callback function or instance method name.
528
529
  * @returns {void}
529
530
  */
530
531
  static beforeCreate(callback) {
@@ -533,9 +534,9 @@ class VelociousDatabaseRecord {
533
534
 
534
535
  /**
535
536
  * Runs before update.
536
- * @template {VelociousDatabaseRecord} T
537
- * @this {ModelConstructor<T> & typeof VelociousDatabaseRecord}
538
- * @param {LifecycleCallbackType<T>} callback - Callback function or instance method name.
537
+ * @template {typeof VelociousDatabaseRecord} MC
538
+ * @this {MC}
539
+ * @param {LifecycleCallbackType<InstanceType<MC>>} callback - Callback function or instance method name.
539
540
  * @returns {void}
540
541
  */
541
542
  static beforeUpdate(callback) {
@@ -544,9 +545,9 @@ class VelociousDatabaseRecord {
544
545
 
545
546
  /**
546
547
  * Runs before destroy.
547
- * @template {VelociousDatabaseRecord} T
548
- * @this {ModelConstructor<T> & typeof VelociousDatabaseRecord}
549
- * @param {LifecycleCallbackType<T>} callback - Callback function or instance method name.
548
+ * @template {typeof VelociousDatabaseRecord} MC
549
+ * @this {MC}
550
+ * @param {LifecycleCallbackType<InstanceType<MC>>} callback - Callback function or instance method name.
550
551
  * @returns {void}
551
552
  */
552
553
  static beforeDestroy(callback) {
@@ -555,9 +556,9 @@ class VelociousDatabaseRecord {
555
556
 
556
557
  /**
557
558
  * Runs after save.
558
- * @template {VelociousDatabaseRecord} T
559
- * @this {ModelConstructor<T> & typeof VelociousDatabaseRecord}
560
- * @param {LifecycleCallbackType<T>} callback - Callback function or instance method name.
559
+ * @template {typeof VelociousDatabaseRecord} MC
560
+ * @this {MC}
561
+ * @param {LifecycleCallbackType<InstanceType<MC>>} callback - Callback function or instance method name.
561
562
  * @returns {void}
562
563
  */
563
564
  static afterSave(callback) {
@@ -566,9 +567,9 @@ class VelociousDatabaseRecord {
566
567
 
567
568
  /**
568
569
  * Runs after create.
569
- * @template {VelociousDatabaseRecord} T
570
- * @this {ModelConstructor<T> & typeof VelociousDatabaseRecord}
571
- * @param {LifecycleCallbackType<T>} callback - Callback function or instance method name.
570
+ * @template {typeof VelociousDatabaseRecord} MC
571
+ * @this {MC}
572
+ * @param {LifecycleCallbackType<InstanceType<MC>>} callback - Callback function or instance method name.
572
573
  * @returns {void}
573
574
  */
574
575
  static afterCreate(callback) {
@@ -577,9 +578,9 @@ class VelociousDatabaseRecord {
577
578
 
578
579
  /**
579
580
  * Runs after update.
580
- * @template {VelociousDatabaseRecord} T
581
- * @this {ModelConstructor<T> & typeof VelociousDatabaseRecord}
582
- * @param {LifecycleCallbackType<T>} callback - Callback function or instance method name.
581
+ * @template {typeof VelociousDatabaseRecord} MC
582
+ * @this {MC}
583
+ * @param {LifecycleCallbackType<InstanceType<MC>>} callback - Callback function or instance method name.
583
584
  * @returns {void}
584
585
  */
585
586
  static afterUpdate(callback) {
@@ -588,9 +589,9 @@ class VelociousDatabaseRecord {
588
589
 
589
590
  /**
590
591
  * Runs after destroy.
591
- * @template {VelociousDatabaseRecord} T
592
- * @this {ModelConstructor<T> & typeof VelociousDatabaseRecord}
593
- * @param {LifecycleCallbackType<T>} callback - Callback function or instance method name.
592
+ * @template {typeof VelociousDatabaseRecord} MC
593
+ * @this {MC}
594
+ * @param {LifecycleCallbackType<InstanceType<MC>>} callback - Callback function or instance method name.
594
595
  * @returns {void}
595
596
  */
596
597
  static afterDestroy(callback) {
@@ -2234,7 +2235,7 @@ class VelociousDatabaseRecord {
2234
2235
  normalizedValue = this._normalizeDateStringForInsert(normalizedValue)
2235
2236
  }
2236
2237
 
2237
- if (normalizedValue instanceof Date) {
2238
+ if (isDate(normalizedValue)) {
2238
2239
  const configuration = this._getConfiguration()
2239
2240
  const offsetMinutes = configuration.getEnvironmentHandler().getTimezoneOffsetMinutes(configuration)
2240
2241
  const offsetMs = offsetMinutes * 60 * 1000
@@ -2437,7 +2438,7 @@ class VelociousDatabaseRecord {
2437
2438
  if (model.isChanged()) {
2438
2439
  await model.save()
2439
2440
 
2440
- const foreignKey = instanceRelationship.getForeignKey()
2441
+ const foreignKey = this._relationshipForeignKeyAttribute(instanceRelationship)
2441
2442
 
2442
2443
  this.setAttribute(foreignKey, model.id())
2443
2444
 
@@ -2492,7 +2493,7 @@ class VelociousDatabaseRecord {
2492
2493
 
2493
2494
  if (loaded) {
2494
2495
  for (const model of loaded) {
2495
- const foreignKey = instanceRelationship.getForeignKey()
2496
+ const foreignKey = model._relationshipForeignKeyAttribute(instanceRelationship)
2496
2497
 
2497
2498
  model.setAttribute(foreignKey, this.id())
2498
2499
 
@@ -2509,6 +2510,17 @@ class VelociousDatabaseRecord {
2509
2510
  return relationships
2510
2511
  }
2511
2512
 
2513
+ /**
2514
+ * Resolves a relationship foreign-key column to this model's public attribute name.
2515
+ * @param {import("./instance-relationships/base.js").default<?, ?>} instanceRelationship - Relationship instance.
2516
+ * @returns {string} Attribute name accepted by setAttribute/assign.
2517
+ */
2518
+ _relationshipForeignKeyAttribute(instanceRelationship) {
2519
+ const foreignKey = instanceRelationship.getForeignKey()
2520
+
2521
+ return this.getModelClass().getColumnNameToAttributeNameMap()[foreignKey] || foreignKey
2522
+ }
2523
+
2512
2524
  /**
2513
2525
  * Runs auto save has many and has one relationships.
2514
2526
  * @param {object} args - Options object.
@@ -2534,7 +2546,7 @@ class VelociousDatabaseRecord {
2534
2546
  }
2535
2547
 
2536
2548
  for (const model of loaded) {
2537
- const foreignKey = instanceRelationship.getForeignKey()
2549
+ const foreignKey = model._relationshipForeignKeyAttribute(instanceRelationship)
2538
2550
 
2539
2551
  model.setAttribute(foreignKey, this.id())
2540
2552
 
@@ -3904,7 +3916,7 @@ class VelociousDatabaseRecord {
3904
3916
  const offsetMinutes = configuration.getEnvironmentHandler().getTimezoneOffsetMinutes(configuration)
3905
3917
  const offsetMs = offsetMinutes * 60 * 1000
3906
3918
 
3907
- if (value instanceof Date) {
3919
+ if (isDate(value)) {
3908
3920
  return new Date(value.getTime() + offsetMs)
3909
3921
  }
3910
3922
 
@@ -4063,7 +4075,7 @@ class VelociousDatabaseRecord {
4063
4075
 
4064
4076
  const value = data[columnName]
4065
4077
 
4066
- if (!(value instanceof Date)) continue
4078
+ if (!isDate(value)) continue
4067
4079
 
4068
4080
  data[columnName] = new Date(value.getTime() - offsetMs)
4069
4081
  }
@@ -21,7 +21,7 @@ export default class VelociousDatabaseRecordBelongsToRelationship extends BaseRe
21
21
  }
22
22
  }
23
23
 
24
- return this.foreignKey
24
+ return this.modelClass.getAttributeNameToColumnNameMap()[this.foreignKey] || this.foreignKey
25
25
  }
26
26
 
27
27
  /**
@@ -13,7 +13,9 @@ export default class VelociousDatabaseRecordHasManyRelationship extends BaseRela
13
13
  this.foreignKey = `${inflection.underscore(this.modelClass.getModelName())}_id`
14
14
  }
15
15
 
16
- return this.foreignKey
16
+ const targetModelClass = this.className || this.klass ? this.getTargetModelClass() : undefined
17
+
18
+ return targetModelClass?.getAttributeNameToColumnNameMap()[this.foreignKey] || this.foreignKey
17
19
  }
18
20
 
19
21
  /**
@@ -13,7 +13,9 @@ export default class VelociousDatabaseRecordHasOneRelationship extends BaseRelat
13
13
  this.foreignKey = `${inflection.underscore(this.modelClass.getModelName())}_id`
14
14
  }
15
15
 
16
- return this.foreignKey
16
+ const targetModelClass = this.className || this.klass ? this.getTargetModelClass() : undefined
17
+
18
+ return targetModelClass?.getAttributeNameToColumnNameMap()[this.foreignKey] || this.foreignKey
17
19
  }
18
20
 
19
21
  /**
@@ -243,6 +243,16 @@ export default class VelociousEnvironmentHandlerBase {
243
243
  throw new Error("cliCommandsGenerateModel not implemented")
244
244
  }
245
245
 
246
+ /**
247
+ * Runs cli commands lint relationships.
248
+ * @abstract
249
+ * @param {import("../cli/base-command.js").default} _command - Command.
250
+ * @returns {Promise<?>} - Resolves with the command result.
251
+ */
252
+ async cliCommandsLintRelationships(_command) {
253
+ throw new Error("cliCommandsLintRelationships not implemented")
254
+ }
255
+
246
256
  /**
247
257
  * Runs cli commands routes.
248
258
  * @abstract
@@ -373,14 +373,14 @@ export default class DbGenerateFrontendModels extends BaseCommand {
373
373
 
374
374
  fileContent += "\n"
375
375
  fileContent += ` /** @returns {${attributesTypeName}[${JSON.stringify(attribute.name)}]} - Attribute value. */\n`
376
- fileContent += ` ${camelizedAttribute}() { return this.readAttribute(${JSON.stringify(attribute.name)}) }\n`
376
+ fileContent += ` ${camelizedAttribute}() { return /** @type {${attributesTypeName}[${JSON.stringify(attribute.name)}]} */ (this.readAttribute(${JSON.stringify(attribute.name)})) }\n`
377
377
 
378
378
  fileContent += "\n"
379
379
  fileContent += " /**\n"
380
380
  fileContent += ` * @param {${attributesTypeName}[${JSON.stringify(attribute.name)}]} newValue - New attribute value.\n`
381
381
  fileContent += ` * @returns {${attributesTypeName}[${JSON.stringify(attribute.name)}]} - Assigned value.\n`
382
382
  fileContent += " */\n"
383
- fileContent += ` set${camelizedAttributeUpper}(newValue) { return this.setAttribute(${JSON.stringify(attribute.name)}, newValue) }\n`
383
+ fileContent += ` set${camelizedAttributeUpper}(newValue) { return /** @type {${attributesTypeName}[${JSON.stringify(attribute.name)}]} */ (this.setAttribute(${JSON.stringify(attribute.name)}, newValue)) }\n`
384
384
  }
385
385
 
386
386
  for (const methodName of Object.keys(collectionCommands)) {
@@ -0,0 +1,144 @@
1
+ // @ts-check
2
+
3
+ import BaseCommand from "../../../../../cli/base-command.js"
4
+ import fs from "node:fs/promises"
5
+ import path from "node:path"
6
+
7
+ /**
8
+ * Lints model relationships: every non-polymorphic belongs-to relationship should have an inverse
9
+ * has-many or has-one relationship declared on its target model class. A missing inverse usually
10
+ * means the target model was never told about the association (e.g. an Event model missing
11
+ * `hasMany("priceCategorySettings")` while PriceCategorySetting declares `belongsTo("event")`).
12
+ *
13
+ * Specific relationships can be ignored through a JSON config file (default:
14
+ * `relationship-lint.json` in the project directory, overridable with `--config <path>`):
15
+ *
16
+ * {"ignore": ["PriceCategorySetting#event"]}
17
+ *
18
+ * where each entry is `<model class name>#<belongs-to relationship name>`.
19
+ */
20
+ export default class VelociousCliCommandsLintRelationships extends BaseCommand {
21
+ /**
22
+ * Runs execute.
23
+ * @returns {Promise<{offences: Array<{ignoreKey: string, message: string}>}>} - Resolves with the found offences (empty when the lint passes).
24
+ */
25
+ async execute() {
26
+ // Relationship target resolution (getTargetModelClass) looks model classes up through the
27
+ // current configuration, so make this command's configuration the current one.
28
+ this.getConfiguration().setCurrent()
29
+
30
+ await this.getConfiguration().initializeModels()
31
+
32
+ const ignoredRelationships = await this._loadIgnoredRelationships()
33
+ const offences = []
34
+ const modelClasses = Object.values(this.getConfiguration().getModelClasses())
35
+
36
+ for (const modelClass of modelClasses) {
37
+ for (const relationship of modelClass.getRelationships()) {
38
+ if (relationship.getType() != "belongsTo") continue
39
+ if (relationship.getPolymorphic()) continue
40
+
41
+ const ignoreKey = `${modelClass.name}#${relationship.getRelationshipName()}`
42
+
43
+ if (ignoredRelationships.has(ignoreKey)) continue
44
+
45
+ let targetModelClass
46
+
47
+ try {
48
+ targetModelClass = relationship.getTargetModelClass()
49
+ } catch (error) {
50
+ offences.push({
51
+ ignoreKey,
52
+ message: `${ignoreKey}: couldn't resolve the target model class: ${error instanceof Error ? error.message : error}`
53
+ })
54
+
55
+ continue
56
+ }
57
+
58
+ if (!targetModelClass) {
59
+ offences.push({ignoreKey, message: `${ignoreKey}: couldn't resolve the target model class`})
60
+
61
+ continue
62
+ }
63
+
64
+ const inverseRelationship = targetModelClass.getRelationships().find((candidate) => {
65
+ if (candidate.getType() != "hasMany" && candidate.getType() != "hasOne") return false
66
+ if (candidate.through) return false
67
+
68
+ try {
69
+ return candidate.getTargetModelClass() === modelClass
70
+ } catch {
71
+ // A has-many/has-one with an unresolvable target can't be the inverse of this belongs-to.
72
+ // It is reported separately when its own model's belongs-to relationships are linted.
73
+ return false
74
+ }
75
+ })
76
+
77
+ if (inverseRelationship) continue
78
+
79
+ offences.push({
80
+ ignoreKey,
81
+ message: `${targetModelClass.name} is missing an inverse hasMany/hasOne relationship for ${ignoreKey} (belongsTo). ` +
82
+ `Declare the inverse on ${targetModelClass.name} or add "${ignoreKey}" to the ignore config.`
83
+ })
84
+ }
85
+ }
86
+
87
+ for (const offence of offences) {
88
+ console.error(offence.message)
89
+ }
90
+
91
+ if (offences.length > 0) {
92
+ throw new Error(`Relationship lint failed with ${offences.length} offence(s):\n${offences.map((offence) => offence.message).join("\n")}`)
93
+ }
94
+
95
+ console.log(`Relationship lint passed for ${modelClasses.length} model(s).`)
96
+
97
+ return {offences}
98
+ }
99
+
100
+ /**
101
+ * Loads the ignored relationship keys from the lint config file. The file is optional; when the
102
+ * default path doesn't exist, no relationships are ignored. An explicitly passed `--config` path
103
+ * must exist.
104
+ * @returns {Promise<Set<string>>} - Ignored `<model>#<relationship>` keys.
105
+ */
106
+ async _loadIgnoredRelationships() {
107
+ const configArgIndex = this.processArgs?.indexOf("--config") ?? -1
108
+ const explicitConfigPath = configArgIndex >= 0 ? this.processArgs?.[configArgIndex + 1] : undefined
109
+
110
+ if (configArgIndex >= 0 && !explicitConfigPath) {
111
+ throw new Error("--config was given without a path argument")
112
+ }
113
+
114
+ const configPath = explicitConfigPath
115
+ ? path.resolve(this.directory(), explicitConfigPath)
116
+ : path.join(this.directory(), "relationship-lint.json")
117
+
118
+ let configContent
119
+
120
+ try {
121
+ configContent = await fs.readFile(configPath, "utf8")
122
+ } catch (error) {
123
+ if (!explicitConfigPath && /** @type {NodeJS.ErrnoException} */ (error).code == "ENOENT") {
124
+ return new Set()
125
+ }
126
+
127
+ throw error
128
+ }
129
+
130
+ const config = JSON.parse(configContent)
131
+
132
+ if (config === null || typeof config != "object" || Array.isArray(config)) {
133
+ throw new Error(`Relationship lint config must be a JSON object: ${configPath}`)
134
+ }
135
+
136
+ const ignore = config.ignore ?? []
137
+
138
+ if (!Array.isArray(ignore) || ignore.some((entry) => typeof entry != "string")) {
139
+ throw new Error(`Relationship lint config "ignore" must be an array of "<model>#<relationship>" strings: ${configPath}`)
140
+ }
141
+
142
+ return new Set(ignore)
143
+ }
144
+ }
@@ -8,6 +8,7 @@ import CliCommandsGenerateBaseModels from "./node/cli/commands/generate/base-mod
8
8
  import CliCommandsGenerateFrontendModels from "./node/cli/commands/generate/frontend-models.js"
9
9
  import CliCommandsGenerateMigration from "./node/cli/commands/generate/migration.js"
10
10
  import CliCommandsGenerateModel from "./node/cli/commands/generate/model.js"
11
+ import CliCommandsLintRelationships from "./node/cli/commands/lint/relationships.js"
11
12
  import CliCommandsRoutes from "./node/cli/commands/routes.js"
12
13
  import CliCommandsServer from "./node/cli/commands/server.js"
13
14
  import CliCommandsTest from "./node/cli/commands/test.js"
@@ -509,6 +510,15 @@ export default class VelociousEnvironmentHandlerNode extends Base{
509
510
  return await this.forwardCommand(command, CliCommandsGenerateModel)
510
511
  }
511
512
 
513
+ /**
514
+ * Runs cli commands lint relationships.
515
+ * @param {import("../cli/base-command.js").default} command - Command.
516
+ * @returns {Promise<?>} - Resolves with the command result.
517
+ */
518
+ async cliCommandsLintRelationships(command) {
519
+ return await this.forwardCommand(command, CliCommandsLintRelationships)
520
+ }
521
+
512
522
  /**
513
523
  * Runs cli commands routes.
514
524
  * @param {import("../cli/base-command.js").default} command - Command.
@@ -9,7 +9,7 @@ import * as inflection from "inflection"
9
9
  * @property {import("../controller.js").default} controller - Frontend-model controller instance.
10
10
  * @property {typeof import("../database/record/index.js").default} modelClass - Backing model class.
11
11
  * @property {string} modelName - Model name.
12
- * @property {import("../configuration-types.js").VelociousLooseObject} params - Request params.
12
+ * @property {import("../configuration-types.js").VelociousParams} params - Request params.
13
13
  * @property {import("../configuration-types.js").NormalizedFrontendModelResourceConfiguration | import("../configuration-types.js").FrontendModelResourceConfiguration} resourceConfiguration - Normalized resource configuration (or raw input shape during early bootstrap).
14
14
  */
15
15
 
@@ -21,13 +21,13 @@ import * as inflection from "inflection"
21
21
  * @property {import("../configuration-types.js").VelociousLooseObject} [locals] - Ability locals.
22
22
  * @property {typeof import("../database/record/index.js").default} [modelClass] - Optional backing model class override.
23
23
  * @property {string} [modelName] - Optional model name override.
24
- * @property {import("../configuration-types.js").VelociousLooseObject} [params] - Optional params override.
24
+ * @property {import("../configuration-types.js").VelociousParams} [params] - Optional params override.
25
25
  * @property {import("../configuration-types.js").NormalizedFrontendModelResourceConfiguration | import("../configuration-types.js").FrontendModelResourceConfiguration} [resourceConfiguration] - Optional normalized resource configuration.
26
26
  */
27
27
 
28
28
  /**
29
29
  * Base class for backend frontend-model resources.
30
- * @template {typeof import("../database/record/index.js").default} [out TModelClass=typeof import("../database/record/index.js").default]
30
+ * @template {typeof import("../database/record/index.js").default} [TModelClass=typeof import("../database/record/index.js").default]
31
31
  */
32
32
  export default class FrontendModelBaseResource extends AuthorizationBaseResource {
33
33
  /**
@@ -99,10 +99,10 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
99
99
  /**
100
100
  * Runs typed controller instance.
101
101
  * @returns {import("../controller.js").default & {
102
- * frontendModelAuthorizedQuery: (action: "index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url") => import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>,
102
+ * frontendModelAuthorizedQuery: (action: "index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url") => import("../database/query/model-class-query.js").default<TModelClass>,
103
103
  * frontendModelAbilityAction: (action: string) => string,
104
104
  * currentAbility: () => import("../authorization/ability.js").default | undefined,
105
- * frontendModelIndexQuery: () => import("../database/query/model-class-query.js").default<typeof import("../database/record/index.js").default>,
105
+ * frontendModelIndexQuery: () => import("../database/query/model-class-query.js").default<TModelClass>,
106
106
  * frontendModelPreload: () => import("../database/query/index.js").NestedPreloadRecord | null,
107
107
  * serializeFrontendModel: (model: import("../database/record/index.js").default) => Promise<Record<string, unknown>>
108
108
  * }} - Controller instance with frontend-model helpers.
@@ -176,9 +176,9 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
176
176
 
177
177
  /**
178
178
  * Runs params.
179
- * @returns {import("../configuration-types.js").VelociousLooseObject} - Params.
179
+ * @returns {import("../configuration-types.js").VelociousParams} - Params.
180
180
  */
181
- params() { return this.paramsValue || super.params() || {} }
181
+ params() { return /** @type {import("../configuration-types.js").VelociousParams} */ (this.paramsValue || super.params() || {}) }
182
182
 
183
183
  /**
184
184
  * Runs resource configuration.
@@ -243,11 +243,10 @@ export default class FrontendModelBaseResource extends AuthorizationBaseResource
243
243
  /**
244
244
  * Runs authorized query.
245
245
  * @param {"index" | "find" | "create" | "update" | "destroy" | "attach" | "download" | "url"} action - Ability action.
246
- * @template {typeof import("../database/record/index.js").default} [MC=typeof import("../database/record/index.js").default]
247
- * @returns {import("../database/query/model-class-query.js").default<MC>} - Authorized query.
246
+ * @returns {import("../database/query/model-class-query.js").default<TModelClass>} - Authorized query.
248
247
  */
249
248
  authorizedQuery(action) {
250
- return /** Narrows the authorized query to the resource's model class. @type {import("../database/query/model-class-query.js").default<MC>} */ (this.typedControllerInstance().frontendModelAuthorizedQuery(action))
249
+ return this.typedControllerInstance().frontendModelAuthorizedQuery(action)
251
250
  }
252
251
 
253
252