velocious 1.0.60 → 1.0.62
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/package.json +2 -2
- package/run-tests.sh +4 -0
- package/spec/cli/commands/db/migrate-spec.js +7 -3
- package/spec/database/record/create-spec.js +12 -0
- package/spec/database/record/query-spec.js +6 -1
- package/spec/dummy/dummy-directory.js +1 -1
- package/spec/dummy/src/database/migrations/20250921121002-create-project-details.js +15 -0
- package/spec/dummy/src/models/project-detail.js +8 -0
- package/spec/dummy/src/models/project.js +1 -0
- package/src/cli/commands/test.js +3 -0
- package/src/configuration.js +4 -4
- package/src/database/drivers/mssql/foreign-key.js +5 -5
- package/src/database/drivers/mssql/index.js +2 -2
- package/src/database/drivers/mssql/options.js +3 -3
- package/src/database/drivers/mysql/foreign-key.js +5 -5
- package/src/database/drivers/mysql/index.js +3 -3
- package/src/database/drivers/pgsql/foreign-key.js +5 -5
- package/src/database/drivers/pgsql/index.js +3 -3
- package/src/database/migration/index.js +5 -0
- package/src/database/pool/async-tracked-multi-connection.js +1 -1
- package/src/database/pool/single-multi-use.js +1 -1
- package/src/database/query/from-plain.js +1 -1
- package/src/database/query/index.js +2 -2
- package/src/database/query/preloader/has-one.js +55 -0
- package/src/database/query/preloader.js +5 -0
- package/src/database/record/index.js +45 -11
- package/src/database/record/instance-relationships/base.js +6 -6
- package/src/database/record/instance-relationships/belongs-to.js +2 -2
- package/src/database/record/instance-relationships/has-many.js +4 -0
- package/src/database/record/instance-relationships/has-one.js +37 -0
- package/src/database/record/relationships/base.js +9 -7
- package/src/database/record/relationships/has-one.js +12 -0
- package/src/templates/generate-migration.js +2 -2
- package/src/testing/test-runner.js +9 -0
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"velocious": "bin/velocious.js"
|
|
4
4
|
},
|
|
5
5
|
"name": "velocious",
|
|
6
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.62",
|
|
7
7
|
"main": "index.js",
|
|
8
8
|
"scripts": {
|
|
9
9
|
"test": "VELOCIOUS_TEST_DIR=../ cd spec/dummy && npx velocious test",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"diggerize": "^1.0.5",
|
|
30
30
|
"ejs": "^3.1.6",
|
|
31
31
|
"env-sense": "^1.0.0",
|
|
32
|
-
"epic-locks": "^1.0.
|
|
32
|
+
"epic-locks": "^1.0.4",
|
|
33
33
|
"escape-string-regexp": "^1.0.5",
|
|
34
34
|
"incorporator": "^1.0.2",
|
|
35
35
|
"inflection": "^3.0.0",
|
package/run-tests.sh
ADDED
|
@@ -18,7 +18,7 @@ describe("Cli - Commands - db:migrate", () => {
|
|
|
18
18
|
await cli.configuration.ensureConnections(async (dbs) => {
|
|
19
19
|
defaultDatabaseType = dbs.default.getType()
|
|
20
20
|
|
|
21
|
-
const tableNames = ["accounts", "authentication_tokens", "tasks", "project_translations", "projects", "schema_migrations", "users"]
|
|
21
|
+
const tableNames = ["accounts", "authentication_tokens", "tasks", "project_details", "project_translations", "projects", "schema_migrations", "users"]
|
|
22
22
|
|
|
23
23
|
for (const tableName of tableNames) {
|
|
24
24
|
await dbs.default.dropTable(tableName, {cascade: true, ifExists: true})
|
|
@@ -102,6 +102,7 @@ describe("Cli - Commands - db:migrate", () => {
|
|
|
102
102
|
[
|
|
103
103
|
"accounts",
|
|
104
104
|
"authentication_tokens",
|
|
105
|
+
"project_details",
|
|
105
106
|
"project_translations",
|
|
106
107
|
"projects",
|
|
107
108
|
"schema_migrations",
|
|
@@ -118,13 +119,15 @@ describe("Cli - Commands - db:migrate", () => {
|
|
|
118
119
|
"20250912183605",
|
|
119
120
|
"20250912183606",
|
|
120
121
|
"20250915085450",
|
|
121
|
-
"20250916111330"
|
|
122
|
+
"20250916111330",
|
|
123
|
+
"20250921121002"
|
|
122
124
|
])
|
|
123
125
|
} else {
|
|
124
126
|
expect(tablesResult.sort()).toEqual(
|
|
125
127
|
[
|
|
126
128
|
"accounts",
|
|
127
129
|
"authentication_tokens",
|
|
130
|
+
"project_details",
|
|
128
131
|
"project_translations",
|
|
129
132
|
"projects",
|
|
130
133
|
"schema_migrations",
|
|
@@ -142,7 +145,8 @@ describe("Cli - Commands - db:migrate", () => {
|
|
|
142
145
|
"20250912183605",
|
|
143
146
|
"20250912183606",
|
|
144
147
|
"20250915085450",
|
|
145
|
-
"20250916111330"
|
|
148
|
+
"20250916111330",
|
|
149
|
+
"20250921121002"
|
|
146
150
|
])
|
|
147
151
|
}
|
|
148
152
|
})
|
|
@@ -2,6 +2,7 @@ import Dummy from "../../dummy/index.js"
|
|
|
2
2
|
import Project from "../../dummy/src/models/project.js"
|
|
3
3
|
import Task from "../../dummy/src/models/task.js"
|
|
4
4
|
import {ValidationError} from "../../../src/database/record/index.js"
|
|
5
|
+
import ProjectDetail from "../../dummy/src/models/project-detail.js"
|
|
5
6
|
|
|
6
7
|
describe("Record - create", () => {
|
|
7
8
|
it("creates a new simple record with relationships and translations", async () => {
|
|
@@ -9,6 +10,8 @@ describe("Record - create", () => {
|
|
|
9
10
|
const task = new Task({name: "Test task"})
|
|
10
11
|
const project = task.buildProject({nameEn: "Test project", nameDe: "Test projekt"})
|
|
11
12
|
|
|
13
|
+
project.buildProjectDetail({note: "Test note"})
|
|
14
|
+
|
|
12
15
|
await task.save()
|
|
13
16
|
|
|
14
17
|
expect(task.id()).not.toBeUndefined()
|
|
@@ -25,6 +28,12 @@ describe("Record - create", () => {
|
|
|
25
28
|
|
|
26
29
|
// 'name' is not a column but rather a column on the translation data model.
|
|
27
30
|
expect(() => project.readColumn("name")).toThrowError("No such attribute or not selected Project#name")
|
|
31
|
+
|
|
32
|
+
// It saves a project note
|
|
33
|
+
const projectDetail = project.projectDetail()
|
|
34
|
+
|
|
35
|
+
expect(projectDetail.note()).toEqual("Test note")
|
|
36
|
+
expect(projectDetail.projectId()).toEqual(project.id())
|
|
28
37
|
})
|
|
29
38
|
})
|
|
30
39
|
|
|
@@ -53,6 +62,7 @@ describe("Record - create", () => {
|
|
|
53
62
|
const project = new Project({name: "Test project"})
|
|
54
63
|
|
|
55
64
|
project.tasks().build({name: " ", project})
|
|
65
|
+
project.buildProjectDetail({note: "Test note"})
|
|
56
66
|
|
|
57
67
|
try {
|
|
58
68
|
await project.save()
|
|
@@ -64,9 +74,11 @@ describe("Record - create", () => {
|
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
const projectsCount = await Project.count()
|
|
77
|
+
const projectDetailsCount = await ProjectDetail.count()
|
|
67
78
|
const tasksCount = await Task.count()
|
|
68
79
|
|
|
69
80
|
expect(projectsCount).toEqual(0)
|
|
81
|
+
expect(projectDetailsCount).toEqual(0)
|
|
70
82
|
expect(tasksCount).toEqual(0)
|
|
71
83
|
})
|
|
72
84
|
})
|
|
@@ -8,6 +8,8 @@ describe("Record - query", () => {
|
|
|
8
8
|
const task = new Task({name: "Test task"})
|
|
9
9
|
const project = task.buildProject({nameEn: "Test project", nameDe: "Test projekt"})
|
|
10
10
|
|
|
11
|
+
project.buildProjectDetail({note: "Test note"})
|
|
12
|
+
|
|
11
13
|
await task.save()
|
|
12
14
|
|
|
13
15
|
expect(task.id()).not.toBeUndefined()
|
|
@@ -20,9 +22,10 @@ describe("Record - query", () => {
|
|
|
20
22
|
expect(project.nameDe()).toEqual("Test projekt")
|
|
21
23
|
expect(project.nameEn()).toEqual("Test project")
|
|
22
24
|
|
|
23
|
-
const tasks = await Task.preload({project: {translations: true}}).toArray()
|
|
25
|
+
const tasks = await Task.preload({project: {projectDetail: true, translations: true}}).toArray()
|
|
24
26
|
const newTask = tasks[0]
|
|
25
27
|
const newProject = newTask.project()
|
|
28
|
+
const newProjectDetail = newProject.projectDetail()
|
|
26
29
|
|
|
27
30
|
expect(newTask.id()).not.toBeUndefined()
|
|
28
31
|
expect(newTask.name()).toEqual("Test task")
|
|
@@ -33,6 +36,8 @@ describe("Record - query", () => {
|
|
|
33
36
|
expect(newProject.name()).toEqual("Test project")
|
|
34
37
|
expect(newProject.nameDe()).toEqual("Test projekt")
|
|
35
38
|
expect(newProject.nameEn()).toEqual("Test project")
|
|
39
|
+
|
|
40
|
+
expect(newProjectDetail.note()).toEqual("Test note")
|
|
36
41
|
})
|
|
37
42
|
})
|
|
38
43
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import Migration from "../../../../../src/database/migration/index.js"
|
|
2
|
+
|
|
3
|
+
export default class CreateProjectDetails extends Migration {
|
|
4
|
+
async up() {
|
|
5
|
+
await this.createTable("project_details", (t) => {
|
|
6
|
+
t.references("project", {foreignKey: true, null: false})
|
|
7
|
+
t.text("note")
|
|
8
|
+
t.timestamps()
|
|
9
|
+
})
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async down() {
|
|
13
|
+
await this.dropTable("project_details")
|
|
14
|
+
}
|
|
15
|
+
}
|
package/src/cli/commands/test.js
CHANGED
|
@@ -22,6 +22,9 @@ export default class VelociousCliCommandsTest extends BaseCommand {
|
|
|
22
22
|
if (testRunner.isFailed()) {
|
|
23
23
|
console.error(`\nTest run failed with ${testRunner.getFailedTests()} failed tests and ${testRunner.getSuccessfulTests()} successfull`)
|
|
24
24
|
process.exit(1)
|
|
25
|
+
} else if (testRunner.areAnyTestsFocussed()) {
|
|
26
|
+
console.error(`\nFocussed run with ${testRunner.getFailedTests()} failed tests and ${testRunner.getSuccessfulTests()} successfull`)
|
|
27
|
+
process.exit(1)
|
|
25
28
|
} else {
|
|
26
29
|
console.log(`\nTest run succeeded with ${testRunner.getSuccessfulTests()} successful tests`)
|
|
27
30
|
process.exit(0)
|
package/src/configuration.js
CHANGED
|
@@ -80,7 +80,7 @@ export default class VelociousConfiguration {
|
|
|
80
80
|
getEnvironment() { return digg(this, "_environment") }
|
|
81
81
|
setEnvironment(newEnvironment) { this._environment = newEnvironment }
|
|
82
82
|
|
|
83
|
-
getLocaleFallbacks
|
|
83
|
+
getLocaleFallbacks() { return this.localeFallbacks }
|
|
84
84
|
setLocaleFallbacks(newLocaleFallbacks) { this.localeFallbacks = newLocaleFallbacks }
|
|
85
85
|
|
|
86
86
|
getLocale() {
|
|
@@ -93,7 +93,7 @@ export default class VelociousConfiguration {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
getLocales
|
|
96
|
+
getLocales() { return digg(this, "locales") }
|
|
97
97
|
|
|
98
98
|
getModelClass(name) {
|
|
99
99
|
const modelClass = this.modelClasses[name]
|
|
@@ -115,8 +115,8 @@ export default class VelociousConfiguration {
|
|
|
115
115
|
this.databasePools[identifier].setCurrent()
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
isDatabasePoolInitialized
|
|
119
|
-
isInitialized
|
|
118
|
+
isDatabasePoolInitialized(identifier = "default") { return Boolean(this.databasePools[identifier]) }
|
|
119
|
+
isInitialized() { return this._isInitialized }
|
|
120
120
|
|
|
121
121
|
async initialize() {
|
|
122
122
|
if (!this.isInitialized()) {
|
|
@@ -2,9 +2,9 @@ import BaseForeignKey from "../base-foreign-key.js"
|
|
|
2
2
|
import {digg} from "diggerize"
|
|
3
3
|
|
|
4
4
|
export default class VelociousDatabaseDriversMssqlForeignKey extends BaseForeignKey {
|
|
5
|
-
getColumnName
|
|
6
|
-
getName
|
|
7
|
-
getTableName
|
|
8
|
-
getReferencedColumnName
|
|
9
|
-
getReferencedTableName
|
|
5
|
+
getColumnName() { return digg(this, "data", "ParentColumn") }
|
|
6
|
+
getName() { return digg(this, "data", "CONSTRAINT_NAME") }
|
|
7
|
+
getTableName() { return digg(this, "data", "TableName") }
|
|
8
|
+
getReferencedColumnName() { return digg(this, "data", "ReferencedColumn") }
|
|
9
|
+
getReferencedTableName() { return digg(this, "data", "ReferencedTable") }
|
|
10
10
|
}
|
|
@@ -82,8 +82,8 @@ export default class VelociousDatabaseDriversMssql extends Base{
|
|
|
82
82
|
return dropTable.toSql()
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
getType
|
|
86
|
-
primaryKeyType
|
|
85
|
+
getType() { return "mssql" }
|
|
86
|
+
primaryKeyType() { return "bigint" }
|
|
87
87
|
|
|
88
88
|
async query(sql) {
|
|
89
89
|
let result, request, tries = 0
|
|
@@ -16,7 +16,7 @@ export default class VelociousDatabaseDriversMssqlOptions extends QueryParserOpt
|
|
|
16
16
|
return this.driver.quote(string)
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
quoteColumnName
|
|
19
|
+
quoteColumnName(string) {
|
|
20
20
|
if (string.includes("[") || string.includes("]")) throw new Error(`Possible SQL injection in column name: ${string}`)
|
|
21
21
|
|
|
22
22
|
return `[${string}]`
|
|
@@ -29,13 +29,13 @@ export default class VelociousDatabaseDriversMssqlOptions extends QueryParserOpt
|
|
|
29
29
|
return `[${databaseName}]`
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
quoteIndexName
|
|
32
|
+
quoteIndexName(string) {
|
|
33
33
|
if (string.includes("[") || string.includes("]")) throw new Error(`Possible SQL injection in index name: ${string}`)
|
|
34
34
|
|
|
35
35
|
return `[${string}]`
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
quoteTableName
|
|
38
|
+
quoteTableName(string) {
|
|
39
39
|
if (string.includes("[") || string.includes("]")) throw new Error(`Possible SQL injection in table name: ${string}`)
|
|
40
40
|
|
|
41
41
|
return `[${string}]`
|
|
@@ -2,9 +2,9 @@ import BaseForeignKey from "../base-foreign-key.js"
|
|
|
2
2
|
import {digg} from "diggerize"
|
|
3
3
|
|
|
4
4
|
export default class VelociousDatabaseDriversMysqlForeignKey extends BaseForeignKey {
|
|
5
|
-
getColumnName
|
|
6
|
-
getName
|
|
7
|
-
getTableName
|
|
8
|
-
getReferencedColumnName
|
|
9
|
-
getReferencedTableName
|
|
5
|
+
getColumnName() { return digg(this, "data", "COLUMN_NAME") }
|
|
6
|
+
getName() { return digg(this, "data", "CONSTRAINT_NAME") }
|
|
7
|
+
getTableName() { return digg(this, "data", "TABLE_NAME") }
|
|
8
|
+
getReferencedColumnName() { return digg(this, "data", "REFERENCED_COLUMN_NAME") }
|
|
9
|
+
getReferencedTableName() { return digg(this, "data", "REFERENCED_TABLE_NAME") }
|
|
10
10
|
}
|
|
@@ -91,8 +91,8 @@ export default class VelociousDatabaseDriversMysql extends Base{
|
|
|
91
91
|
return dropTable.toSql()
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
getType
|
|
95
|
-
primaryKeyType
|
|
94
|
+
getType() { return "mysql" }
|
|
95
|
+
primaryKeyType() { return "bigint" }
|
|
96
96
|
|
|
97
97
|
async query(sql) {
|
|
98
98
|
try {
|
|
@@ -107,7 +107,7 @@ export default class VelociousDatabaseDriversMysql extends Base{
|
|
|
107
107
|
return new QueryParser({query}).toSql()
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
shouldSetAutoIncrementWhenPrimaryKey
|
|
110
|
+
shouldSetAutoIncrementWhenPrimaryKey() { return true }
|
|
111
111
|
|
|
112
112
|
escape(value) {
|
|
113
113
|
if (!this.connection) throw new Error("Can't escape before connected")
|
|
@@ -2,9 +2,9 @@ import BaseForeignKey from "../base-foreign-key.js"
|
|
|
2
2
|
import {digg} from "diggerize"
|
|
3
3
|
|
|
4
4
|
export default class VelociousDatabaseDriversPgsqlForeignKey extends BaseForeignKey {
|
|
5
|
-
getColumnName
|
|
6
|
-
getName
|
|
7
|
-
getTableName
|
|
8
|
-
getReferencedColumnName
|
|
9
|
-
getReferencedTableName
|
|
5
|
+
getColumnName() { return digg(this, "data", "column_name") }
|
|
6
|
+
getName() { return digg(this, "data", "constraint_name") }
|
|
7
|
+
getTableName() { return digg(this, "data", "table_name") }
|
|
8
|
+
getReferencedColumnName() { return digg(this, "data", "foreign_column_name") }
|
|
9
|
+
getReferencedTableName() { return digg(this, "data", "foreign_table_name") }
|
|
10
10
|
}
|
|
@@ -99,8 +99,8 @@ export default class VelociousDatabaseDriversPgsql extends Base{
|
|
|
99
99
|
return dropTable.toSql()
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
getType
|
|
103
|
-
primaryKeyType
|
|
102
|
+
getType() { return "pgsql" }
|
|
103
|
+
primaryKeyType() { return "bigint" }
|
|
104
104
|
|
|
105
105
|
async query(sql) {
|
|
106
106
|
let response
|
|
@@ -118,7 +118,7 @@ export default class VelociousDatabaseDriversPgsql extends Base{
|
|
|
118
118
|
return new QueryParser({query}).toSql()
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
shouldSetAutoIncrementWhenPrimaryKey
|
|
121
|
+
shouldSetAutoIncrementWhenPrimaryKey() { return true }
|
|
122
122
|
|
|
123
123
|
escape(value) {
|
|
124
124
|
if (!this.connection) throw new Error("Can't escape before connected")
|
|
@@ -27,6 +27,11 @@ export default class VelociousDatabaseMigration {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
getDriver() { return this._db }
|
|
30
|
+
connection() { return this.getDriver() }
|
|
31
|
+
|
|
32
|
+
async execute(sql) {
|
|
33
|
+
await this.connection().query(sql)
|
|
34
|
+
}
|
|
30
35
|
|
|
31
36
|
async addColumn(tableName, columnName, columnType, args) {
|
|
32
37
|
if (!columnType) throw new Error("No column type given")
|
|
@@ -71,7 +71,7 @@ export default class VelociousDatabaseQuery {
|
|
|
71
71
|
throw new Error("count multiple stub")
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
-
getOptions
|
|
74
|
+
getOptions() { return this.driver.options() }
|
|
75
75
|
|
|
76
76
|
async destroyAll() {
|
|
77
77
|
const records = await this.toArray()
|
|
@@ -255,7 +255,7 @@ export default class VelociousDatabaseQuery {
|
|
|
255
255
|
return models
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
toSql
|
|
258
|
+
toSql() { return this.driver.queryToSql(this) }
|
|
259
259
|
|
|
260
260
|
where(where) {
|
|
261
261
|
if (typeof where == "string") {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as inflection from "inflection"
|
|
2
|
+
import restArgsError from "../../../utils/rest-args-error.js"
|
|
3
|
+
|
|
4
|
+
export default class VelociousDatabaseQueryPreloaderHasOne {
|
|
5
|
+
constructor({models, relationship, ...restArgs}) {
|
|
6
|
+
restArgsError(restArgs)
|
|
7
|
+
|
|
8
|
+
this.models = models
|
|
9
|
+
this.relationship = relationship
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async run() {
|
|
13
|
+
const modelIds = []
|
|
14
|
+
const modelsById = {}
|
|
15
|
+
const foreignKey = this.relationship.getForeignKey()
|
|
16
|
+
const foreignKeyCamelized = inflection.camelize(foreignKey, true)
|
|
17
|
+
const preloadCollections = {}
|
|
18
|
+
|
|
19
|
+
for (const model of this.models) {
|
|
20
|
+
preloadCollections[model.id()] = null
|
|
21
|
+
modelIds.push(model.id())
|
|
22
|
+
|
|
23
|
+
if (!(model.id in modelsById)) modelsById[model.id()] = []
|
|
24
|
+
|
|
25
|
+
modelsById[model.id()].push(model)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const whereArgs = {}
|
|
29
|
+
|
|
30
|
+
whereArgs[foreignKey] = modelIds
|
|
31
|
+
|
|
32
|
+
// Load target models to be preloaded on the given models
|
|
33
|
+
const targetModels = await this.relationship.getTargetModelClass().where(whereArgs).toArray()
|
|
34
|
+
|
|
35
|
+
for (const targetModel of targetModels) {
|
|
36
|
+
const foreignKeyValue = targetModel[foreignKeyCamelized]()
|
|
37
|
+
|
|
38
|
+
preloadCollections[foreignKeyValue] = targetModel
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Set the target preloaded models on the given models
|
|
42
|
+
for (const modelId in preloadCollections) {
|
|
43
|
+
const preloadedModel = preloadCollections[modelId]
|
|
44
|
+
|
|
45
|
+
for (const model of modelsById[modelId]) {
|
|
46
|
+
const modelRelationship = model.getRelationshipByName(this.relationship.getRelationshipName())
|
|
47
|
+
|
|
48
|
+
modelRelationship.setPreloaded(true)
|
|
49
|
+
modelRelationship.setLoaded(preloadedModel)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return targetModels
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import BelongsToPreloader from "./preloader/belongs-to.js"
|
|
2
2
|
import HasManyPreloader from "./preloader/has-many.js"
|
|
3
|
+
import HasOnePreloader from "./preloader/has-one.js"
|
|
3
4
|
import restArgsError from "../../utils/rest-args-error.js"
|
|
4
5
|
|
|
5
6
|
export default class VelociousDatabaseQueryPreloader {
|
|
@@ -24,6 +25,10 @@ export default class VelociousDatabaseQueryPreloader {
|
|
|
24
25
|
const hasManyPreloader = new HasManyPreloader({models: this.models, relationship: relationship})
|
|
25
26
|
|
|
26
27
|
targetModels = await hasManyPreloader.run()
|
|
28
|
+
} else if (relationship.getType() == "hasOne") {
|
|
29
|
+
const hasOnePreloader = new HasOnePreloader({models: this.models, relationship: relationship})
|
|
30
|
+
|
|
31
|
+
targetModels = await hasOnePreloader.run()
|
|
27
32
|
} else {
|
|
28
33
|
throw new Error(`Unknown relationship type: ${relationship.getType()}`)
|
|
29
34
|
}
|
|
@@ -5,6 +5,8 @@ import FromTable from "../query/from-table.js"
|
|
|
5
5
|
import Handler from "../handler.js"
|
|
6
6
|
import HasManyRelationship from "./relationships/has-many.js"
|
|
7
7
|
import HasManyInstanceRelationship from "./instance-relationships/has-many.js"
|
|
8
|
+
import HasOneRelationship from "./relationships/has-one.js"
|
|
9
|
+
import HasOneInstanceRelationship from "./instance-relationships/has-one.js"
|
|
8
10
|
import * as inflection from "inflection"
|
|
9
11
|
import Query from "../query/index.js"
|
|
10
12
|
import ValidatorsPresence from "./validators/presence.js"
|
|
@@ -104,6 +106,21 @@ class VelociousDatabaseRecord {
|
|
|
104
106
|
this.prototype[relationshipName] = function () {
|
|
105
107
|
return this.getRelationshipByName(relationshipName)
|
|
106
108
|
}
|
|
109
|
+
} else if (actualData.type == "hasOne") {
|
|
110
|
+
const buildMethodName = `build${inflection.camelize(relationshipName)}`
|
|
111
|
+
|
|
112
|
+
relationship = new HasOneRelationship(actualData)
|
|
113
|
+
|
|
114
|
+
this.prototype[relationshipName] = function () {
|
|
115
|
+
return this.getRelationshipByName(relationshipName).loaded()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.prototype[buildMethodName] = function (attributes) {
|
|
119
|
+
const relationship = this.getRelationshipByName(relationshipName)
|
|
120
|
+
const record = relationship.build(attributes)
|
|
121
|
+
|
|
122
|
+
return record
|
|
123
|
+
}
|
|
107
124
|
} else {
|
|
108
125
|
throw new Error(`Unknown relationship type: ${actualData.type}`)
|
|
109
126
|
}
|
|
@@ -132,14 +149,17 @@ class VelociousDatabaseRecord {
|
|
|
132
149
|
|
|
133
150
|
if (!(relationshipName in this._instanceRelationships)) {
|
|
134
151
|
const modelClassRelationship = this.constructor.getRelationshipByName(relationshipName)
|
|
152
|
+
const relationshipType = modelClassRelationship.getType()
|
|
135
153
|
let instanceRelationship
|
|
136
154
|
|
|
137
|
-
if (
|
|
155
|
+
if (relationshipType == "belongsTo") {
|
|
138
156
|
instanceRelationship = new BelongsToInstanceRelationship({model: this, relationship: modelClassRelationship})
|
|
139
|
-
} else if (
|
|
157
|
+
} else if (relationshipType == "hasMany") {
|
|
140
158
|
instanceRelationship = new HasManyInstanceRelationship({model: this, relationship: modelClassRelationship})
|
|
159
|
+
} else if (relationshipType == "hasOne") {
|
|
160
|
+
instanceRelationship = new HasOneInstanceRelationship({model: this, relationship: modelClassRelationship})
|
|
141
161
|
} else {
|
|
142
|
-
throw new Error(`Unknown relationship type: ${
|
|
162
|
+
throw new Error(`Unknown relationship type: ${relationshipType}`)
|
|
143
163
|
}
|
|
144
164
|
|
|
145
165
|
this._instanceRelationships[relationshipName] = instanceRelationship
|
|
@@ -189,6 +209,10 @@ class VelociousDatabaseRecord {
|
|
|
189
209
|
return this._defineRelationship(relationshipName, Object.assign({type: "hasMany"}, options))
|
|
190
210
|
}
|
|
191
211
|
|
|
212
|
+
static hasOne(relationshipName, options = {}) {
|
|
213
|
+
return this._defineRelationship(relationshipName, Object.assign({type: "hasOne"}, options))
|
|
214
|
+
}
|
|
215
|
+
|
|
192
216
|
static humanAttributeName(attributeName) {
|
|
193
217
|
const modelNameKey = inflection.underscore(this.constructor.name)
|
|
194
218
|
|
|
@@ -374,7 +398,7 @@ class VelociousDatabaseRecord {
|
|
|
374
398
|
|
|
375
399
|
if (this.isPersisted()) {
|
|
376
400
|
// If any has-many-relationships will be saved, then updated-at should still be set on this record.
|
|
377
|
-
const autoSaveHasManyrelationships = this.
|
|
401
|
+
const autoSaveHasManyrelationships = this._autoSaveHasManyAndHasOneRelationshipsToSave()
|
|
378
402
|
|
|
379
403
|
if (this._hasChanges() || savedCount > 0 || autoSaveHasManyrelationships.length > 0) {
|
|
380
404
|
result = await this._updateRecordWithChanges()
|
|
@@ -383,7 +407,7 @@ class VelociousDatabaseRecord {
|
|
|
383
407
|
result = await this._createNewRecord()
|
|
384
408
|
}
|
|
385
409
|
|
|
386
|
-
await this.
|
|
410
|
+
await this._autoSaveHasManyAndHasOneRelationships({isNewRecord})
|
|
387
411
|
})
|
|
388
412
|
|
|
389
413
|
return result
|
|
@@ -417,19 +441,29 @@ class VelociousDatabaseRecord {
|
|
|
417
441
|
return {savedCount}
|
|
418
442
|
}
|
|
419
443
|
|
|
420
|
-
|
|
444
|
+
_autoSaveHasManyAndHasOneRelationshipsToSave() {
|
|
421
445
|
const relationships = []
|
|
422
446
|
|
|
423
447
|
for (const relationshipName in this._instanceRelationships) {
|
|
424
448
|
const instanceRelationship = this._instanceRelationships[relationshipName]
|
|
425
449
|
|
|
426
|
-
if (instanceRelationship.getType() != "hasMany") {
|
|
450
|
+
if (instanceRelationship.getType() != "hasMany" && instanceRelationship.getType() != "hasOne") {
|
|
427
451
|
continue
|
|
428
452
|
}
|
|
429
453
|
|
|
430
|
-
let loaded
|
|
454
|
+
let loaded
|
|
431
455
|
|
|
432
|
-
if (
|
|
456
|
+
if (instanceRelationship.getType() == "hasOne") {
|
|
457
|
+
const hasOneLoaded = instanceRelationship.getLoadedOrNull()
|
|
458
|
+
|
|
459
|
+
if (hasOneLoaded) {
|
|
460
|
+
loaded = [hasOneLoaded]
|
|
461
|
+
} else {
|
|
462
|
+
continue
|
|
463
|
+
}
|
|
464
|
+
} else {
|
|
465
|
+
loaded = instanceRelationship.getLoadedOrNull()
|
|
466
|
+
}
|
|
433
467
|
|
|
434
468
|
let useRelationship = false
|
|
435
469
|
|
|
@@ -450,8 +484,8 @@ class VelociousDatabaseRecord {
|
|
|
450
484
|
return relationships
|
|
451
485
|
}
|
|
452
486
|
|
|
453
|
-
async
|
|
454
|
-
for (const instanceRelationship of this.
|
|
487
|
+
async _autoSaveHasManyAndHasOneRelationships({isNewRecord}) {
|
|
488
|
+
for (const instanceRelationship of this._autoSaveHasManyAndHasOneRelationshipsToSave()) {
|
|
455
489
|
let loaded = instanceRelationship._loaded
|
|
456
490
|
|
|
457
491
|
if (!Array.isArray(loaded)) loaded = [loaded]
|
|
@@ -9,7 +9,7 @@ export default class VelociousDatabaseRecordBaseInstanceRelationship {
|
|
|
9
9
|
this._dirty = newValue
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
getDirty
|
|
12
|
+
getDirty() { return this._dirty }
|
|
13
13
|
|
|
14
14
|
loaded() {
|
|
15
15
|
if (!this._preloaded && this.model.isPersisted()) {
|
|
@@ -27,9 +27,9 @@ export default class VelociousDatabaseRecordBaseInstanceRelationship {
|
|
|
27
27
|
this._preloaded = preloadedValue
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
getForeignKey
|
|
31
|
-
getPrimaryKey
|
|
32
|
-
getRelationship
|
|
33
|
-
getTargetModelClass
|
|
34
|
-
getType
|
|
30
|
+
getForeignKey() { return this.getRelationship().getForeignKey() }
|
|
31
|
+
getPrimaryKey() { return this.getRelationship().getPrimaryKey() }
|
|
32
|
+
getRelationship() { return this.relationship }
|
|
33
|
+
getTargetModelClass() { return this.getRelationship().getTargetModelClass() }
|
|
34
|
+
getType() { return this.getRelationship().getType() }
|
|
35
35
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import BaseInstanceRelationship from "./base.js"
|
|
2
|
+
|
|
3
|
+
export default class VelociousDatabaseRecordHasOneInstanceRelationship extends BaseInstanceRelationship {
|
|
4
|
+
constructor(args) {
|
|
5
|
+
super(args)
|
|
6
|
+
this._loaded = null
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
build(data) {
|
|
10
|
+
const targetModelClass = this.getTargetModelClass()
|
|
11
|
+
const newInstance = new targetModelClass(data)
|
|
12
|
+
|
|
13
|
+
this._loaded = newInstance
|
|
14
|
+
|
|
15
|
+
return newInstance
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
loaded() {
|
|
19
|
+
if (!this._preloaded && this.model.isPersisted()) {
|
|
20
|
+
throw new Error(`${this.model.constructor.name}#${this.relationship.getRelationshipName()} hasn't been preloaded`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return this._loaded
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getLoadedOrNull() {
|
|
27
|
+
return this._loaded
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
setLoaded(model) {
|
|
31
|
+
if (Array.isArray(model)) throw new Error(`Argument given to setLoaded was an array: ${typeof model}`)
|
|
32
|
+
|
|
33
|
+
this._loaded = model
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
getTargetModelClass = () => this.relationship.getTargetModelClass()
|
|
37
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import restArgsError from "../../../utils/rest-args-error.js"
|
|
2
|
+
|
|
1
3
|
export default class VelociousDatabaseRecordBaseRelationship {
|
|
2
|
-
constructor({className, configuration, dependent, foreignKey, klass, modelClass, relationshipName, through, type, ...restArgs}) {
|
|
3
|
-
|
|
4
|
+
constructor({className, configuration, dependent, foreignKey, klass, modelClass, primaryKey = "id", relationshipName, through, type, ...restArgs}) {
|
|
5
|
+
restArgsError(restArgs)
|
|
4
6
|
|
|
5
|
-
if (restArgsKeys.length > 0) throw new Error(`Unknown args given: ${restArgsKeys.join(", ")}`)
|
|
6
7
|
if (!modelClass) throw new Error(`'modelClass' wasn't given for ${relationshipName}`)
|
|
7
8
|
if (!className && !klass) throw new Error(`Neither 'className' or 'klass' was given for ${modelClass.name}#${relationshipName}`)
|
|
8
9
|
|
|
@@ -12,15 +13,16 @@ export default class VelociousDatabaseRecordBaseRelationship {
|
|
|
12
13
|
this.foreignKey = foreignKey
|
|
13
14
|
this.klass = klass
|
|
14
15
|
this.modelClass = modelClass
|
|
16
|
+
this._primaryKey = primaryKey
|
|
15
17
|
this.relationshipName = relationshipName
|
|
16
18
|
this.through = through
|
|
17
19
|
this.type = type
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
getDependent
|
|
21
|
-
getRelationshipName
|
|
22
|
-
getPrimaryKey
|
|
23
|
-
getType
|
|
22
|
+
getDependent() { return this._dependent }
|
|
23
|
+
getRelationshipName() { return this.relationshipName }
|
|
24
|
+
getPrimaryKey() { return this._primaryKey }
|
|
25
|
+
getType() { return this.type }
|
|
24
26
|
|
|
25
27
|
getTargetModelClass() {
|
|
26
28
|
if (this.className) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import BaseRelationship from "./base.js"
|
|
2
|
+
import * as inflection from "inflection"
|
|
3
|
+
|
|
4
|
+
export default class VelociousDatabaseRecordHasOneRelationship extends BaseRelationship {
|
|
5
|
+
getForeignKey() {
|
|
6
|
+
if (!this.foreignKey) {
|
|
7
|
+
this.foreignKey = `${inflection.underscore(this.modelClass.name)}_id`
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return this.foreignKey
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -2,10 +2,10 @@ import Migration from "velocious/src/database/migration/index.js"
|
|
|
2
2
|
|
|
3
3
|
export default class __MIGRATION_NAME__ extends Migration {
|
|
4
4
|
async up() {
|
|
5
|
-
await this.
|
|
5
|
+
await this.execute("...")
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
async down() {
|
|
9
|
-
await this.
|
|
9
|
+
await this.execute("...")
|
|
10
10
|
}
|
|
11
11
|
}
|
|
@@ -62,6 +62,7 @@ export default class TestRunner {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
async prepare() {
|
|
65
|
+
this.anyTestsFocussed = false
|
|
65
66
|
this._failedTests = 0
|
|
66
67
|
this._successfulTests = 0
|
|
67
68
|
this._testsCount = 0
|
|
@@ -76,6 +77,14 @@ export default class TestRunner {
|
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
areAnyTestsFocussed() {
|
|
81
|
+
if (this.anyTestsFocussed === undefined) {
|
|
82
|
+
throw new Error("Hasn't been detected yet")
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return this.anyTestsFocussed
|
|
86
|
+
}
|
|
87
|
+
|
|
79
88
|
async run() {
|
|
80
89
|
await this.configuration.ensureConnections(async () => {
|
|
81
90
|
await this.runTests({
|