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.
- package/build/cli/commands/lint/relationships.js +12 -0
- package/build/database/drivers/base.js +4 -1
- package/build/database/record/index.js +4 -3
- package/build/environment-handlers/base.js +10 -0
- package/build/environment-handlers/node/cli/commands/lint/relationships.js +144 -0
- package/build/environment-handlers/node.js +10 -0
- package/build/src/cli/commands/lint/relationships.d.ts +5 -0
- package/build/src/cli/commands/lint/relationships.d.ts.map +1 -0
- package/build/src/cli/commands/lint/relationships.js +12 -0
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +5 -2
- package/build/src/database/record/index.d.ts.map +1 -1
- package/build/src/database/record/index.js +5 -4
- package/build/src/environment-handlers/base.d.ts +7 -0
- package/build/src/environment-handlers/base.d.ts.map +1 -1
- package/build/src/environment-handlers/base.js +10 -1
- package/build/src/environment-handlers/node/cli/commands/lint/relationships.d.ts +34 -0
- package/build/src/environment-handlers/node/cli/commands/lint/relationships.d.ts.map +1 -0
- package/build/src/environment-handlers/node/cli/commands/lint/relationships.js +123 -0
- package/build/src/environment-handlers/node.d.ts.map +1 -1
- package/build/src/environment-handlers/node.js +10 -1
- package/build/src/utils/is-date.d.ts +10 -0
- package/build/src/utils/is-date.d.ts.map +1 -0
- package/build/src/utils/is-date.js +13 -0
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/utils/is-date.js +13 -0
- package/package.json +1 -1
- package/src/cli/commands/lint/relationships.js +12 -0
- package/src/database/drivers/base.js +4 -1
- package/src/database/record/index.js +4 -3
- package/src/environment-handlers/base.js +10 -0
- package/src/environment-handlers/node/cli/commands/lint/relationships.js +144 -0
- package/src/environment-handlers/node.js +10 -0
- 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
|
+
}
|