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
@@ -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
 
@@ -2660,8 +2660,8 @@ export default class FrontendModelBase {
2660
2660
  * `openConnection`. Apps use this for per-session state/messaging
2661
2661
  * that doesn't fit the pub/sub Channel model (locale, presence).
2662
2662
  * @param {string} connectionType - Name the server registered the class under.
2663
- * @param {{params?: Record<string, ?>, onConnect?: () => void, onMessage?: (body: ?) => void, onDisconnect?: () => void, onResume?: () => void, onClose?: (reason: string) => void}} [options] - Connection options and event handlers.
2664
- * @returns {?} - VelociousWebsocketClientConnection handle (typed loosely to avoid a cross-module import cycle).
2663
+ * @param {{params?: Record<string, ?>, onConnect?: () => void, onMessage?: (body: Record<string, unknown>) => void, onDisconnect?: () => void, onResume?: () => void, onClose?: (reason: string) => void}} [options] - Connection options and event handlers.
2664
+ * @returns {{ready: Promise<void>, close: () => void}} - Websocket connection handle.
2665
2665
  */
2666
2666
  static openWebsocketConnection(connectionType, options) {
2667
2667
  const client = /**
@@ -2679,8 +2679,8 @@ export default class FrontendModelBase {
2679
2679
  * Subscribes to a pub/sub `WebsocketChannel`. Thin wrapper around
2680
2680
  * the internal client's `subscribeChannel`.
2681
2681
  * @param {string} channelType - Channel class name registered on the server.
2682
- * @param {{params?: Record<string, ?>, onMessage?: (body: ?) => void, onDisconnect?: () => void, onResume?: () => void, onClose?: (reason: string) => void}} [options] - Channel subscription options and event handlers.
2683
- * @returns {?} - Websocket channel handle from the configured client.
2682
+ * @param {{params?: Record<string, ?>, onMessage?: (body: Record<string, unknown>) => void, onDisconnect?: () => void, onResume?: () => void, onClose?: (reason: string) => void}} [options] - Channel subscription options and event handlers.
2683
+ * @returns {{ready: Promise<void>, close: () => void}} - Websocket channel handle from the configured client.
2684
2684
  */
2685
2685
  static subscribeWebsocketChannel(channelType, options) {
2686
2686
  const client = /**
@@ -3200,7 +3200,7 @@ export default class FrontendModelBase {
3200
3200
  * Runs sort.
3201
3201
  * @template {typeof FrontendModelBase} T
3202
3202
  * @this {T}
3203
- * @param {string | string[] | [string, string] | Array<[string, string]> | Record<string, ?> | Array<Record<string, ?>>} sort - Sort definition(s).
3203
+ * @param {string | string[] | string[][] | [string, string] | Array<[string, string]> | Record<string, ?> | Array<Record<string, ?>>} sort - Sort definition(s).
3204
3204
  * @returns {FrontendModelQuery<T>} - Query builder with sort definitions.
3205
3205
  */
3206
3206
  static sort(sort) {
@@ -3211,7 +3211,7 @@ export default class FrontendModelBase {
3211
3211
  * Runs order.
3212
3212
  * @template {typeof FrontendModelBase} T
3213
3213
  * @this {T}
3214
- * @param {string | string[] | [string, string] | Array<[string, string]> | Record<string, ?> | Array<Record<string, ?>>} sort - Sort definition(s).
3214
+ * @param {string | string[] | string[][] | [string, string] | Array<[string, string]> | Record<string, ?> | Array<Record<string, ?>>} sort - Sort definition(s).
3215
3215
  * @returns {FrontendModelQuery<T>} - Query builder with sort definitions.
3216
3216
  */
3217
3217
  static order(sort) {
@@ -1414,7 +1414,7 @@ export default class FrontendModelQuery {
1414
1414
 
1415
1415
  /**
1416
1416
  * Runs sort.
1417
- * @param {string | string[] | [string, string] | Array<[string, string]> | Record<string, ?> | Array<Record<string, ?>>} sort - Sort definition(s).
1417
+ * @param {string | string[] | string[][] | [string, string] | Array<[string, string]> | Record<string, ?> | Array<Record<string, ?>>} sort - Sort definition(s).
1418
1418
  * @returns {this} - Query with appended sort definitions.
1419
1419
  */
1420
1420
  sort(sort) {
@@ -1425,7 +1425,7 @@ export default class FrontendModelQuery {
1425
1425
 
1426
1426
  /**
1427
1427
  * Runs order.
1428
- * @param {string | string[] | [string, string] | Array<[string, string]> | Record<string, ?> | Array<Record<string, ?>>} order - Order definition(s).
1428
+ * @param {string | string[] | string[][] | [string, string] | Array<[string, string]> | Record<string, ?> | Array<Record<string, ?>>} order - Order definition(s).
1429
1429
  * @returns {this} - Query with appended sort definitions.
1430
1430
  */
1431
1431
  order(order) {
@@ -0,0 +1,13 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * Whether the value is a Date, including Dates created in another JS realm (e.g. the velocious
5
+ * console REPL context or a node:vm context), where `instanceof Date` is false because the other
6
+ * realm has its own Date constructor. Without this, such a Date bypasses date normalization and SQL
7
+ * value conversion and ends up as an empty value in the generated SQL.
8
+ * @param {?} value - Value to test.
9
+ * @returns {value is Date} - Whether the value is a Date from any realm.
10
+ */
11
+ export default function isDate(value) {
12
+ return value instanceof Date || Object.prototype.toString.call(value) === "[object Date]"
13
+ }