velocious 1.0.441 → 1.0.442

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 (34) hide show
  1. package/build/cli/commands/lint/relationships.js +12 -0
  2. package/build/database/drivers/base.js +4 -1
  3. package/build/database/record/index.js +4 -3
  4. package/build/environment-handlers/base.js +10 -0
  5. package/build/environment-handlers/node/cli/commands/lint/relationships.js +144 -0
  6. package/build/environment-handlers/node.js +10 -0
  7. package/build/src/cli/commands/lint/relationships.d.ts +5 -0
  8. package/build/src/cli/commands/lint/relationships.d.ts.map +1 -0
  9. package/build/src/cli/commands/lint/relationships.js +12 -0
  10. package/build/src/database/drivers/base.d.ts.map +1 -1
  11. package/build/src/database/drivers/base.js +5 -2
  12. package/build/src/database/record/index.d.ts.map +1 -1
  13. package/build/src/database/record/index.js +5 -4
  14. package/build/src/environment-handlers/base.d.ts +7 -0
  15. package/build/src/environment-handlers/base.d.ts.map +1 -1
  16. package/build/src/environment-handlers/base.js +10 -1
  17. package/build/src/environment-handlers/node/cli/commands/lint/relationships.d.ts +34 -0
  18. package/build/src/environment-handlers/node/cli/commands/lint/relationships.d.ts.map +1 -0
  19. package/build/src/environment-handlers/node/cli/commands/lint/relationships.js +123 -0
  20. package/build/src/environment-handlers/node.d.ts.map +1 -1
  21. package/build/src/environment-handlers/node.js +10 -1
  22. package/build/src/utils/is-date.d.ts +10 -0
  23. package/build/src/utils/is-date.d.ts.map +1 -0
  24. package/build/src/utils/is-date.js +13 -0
  25. package/build/tsconfig.tsbuildinfo +1 -1
  26. package/build/utils/is-date.js +13 -0
  27. package/package.json +1 -1
  28. package/src/cli/commands/lint/relationships.js +12 -0
  29. package/src/database/drivers/base.js +4 -1
  30. package/src/database/record/index.js +4 -3
  31. package/src/environment-handlers/base.js +10 -0
  32. package/src/environment-handlers/node/cli/commands/lint/relationships.js +144 -0
  33. package/src/environment-handlers/node.js +10 -0
  34. package/src/utils/is-date.js +13 -0
@@ -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
@@ -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.
@@ -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
+ }