velocious 1.0.60 → 1.0.61

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 (30) hide show
  1. package/package.json +2 -2
  2. package/run-tests.sh +4 -0
  3. package/spec/cli/commands/db/migrate-spec.js +7 -3
  4. package/spec/database/record/create-spec.js +12 -0
  5. package/spec/database/record/query-spec.js +6 -1
  6. package/spec/dummy/dummy-directory.js +1 -1
  7. package/spec/dummy/src/database/migrations/20250921121002-create-project-details.js +15 -0
  8. package/spec/dummy/src/models/project-detail.js +8 -0
  9. package/spec/dummy/src/models/project.js +1 -0
  10. package/src/configuration.js +4 -4
  11. package/src/database/drivers/mssql/foreign-key.js +5 -5
  12. package/src/database/drivers/mssql/index.js +2 -2
  13. package/src/database/drivers/mssql/options.js +3 -3
  14. package/src/database/drivers/mysql/foreign-key.js +5 -5
  15. package/src/database/drivers/mysql/index.js +3 -3
  16. package/src/database/drivers/pgsql/foreign-key.js +5 -5
  17. package/src/database/drivers/pgsql/index.js +3 -3
  18. package/src/database/pool/async-tracked-multi-connection.js +1 -1
  19. package/src/database/pool/single-multi-use.js +1 -1
  20. package/src/database/query/from-plain.js +1 -1
  21. package/src/database/query/index.js +2 -2
  22. package/src/database/query/preloader/has-one.js +55 -0
  23. package/src/database/query/preloader.js +5 -0
  24. package/src/database/record/index.js +45 -11
  25. package/src/database/record/instance-relationships/base.js +6 -6
  26. package/src/database/record/instance-relationships/belongs-to.js +2 -2
  27. package/src/database/record/instance-relationships/has-many.js +4 -0
  28. package/src/database/record/instance-relationships/has-one.js +37 -0
  29. package/src/database/record/relationships/base.js +9 -7
  30. package/src/database/record/relationships/has-one.js +12 -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.60",
6
+ "version": "1.0.61",
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.3",
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
@@ -0,0 +1,4 @@
1
+ #!/bin/bash
2
+
3
+ VELOCIOUS_TEST_DIR=/home/dev/Development/velocious cd spec/dummy && npx velocious test
4
+
@@ -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
 
@@ -1,7 +1,7 @@
1
1
  import {dirname} from "path"
2
2
  import {fileURLToPath} from "url"
3
3
 
4
- const dummyDirectory = () => {
4
+ function dummyDirectory() {
5
5
  const __filename = fileURLToPath(import.meta.url)
6
6
  const __dirname = dirname(__filename)
7
7
 
@@ -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
+ }
@@ -0,0 +1,8 @@
1
+ import DatabaseRecord from "../../../../src/database/record/index.js"
2
+
3
+ class ProjectDetail extends DatabaseRecord {
4
+ }
5
+
6
+ ProjectDetail.belongsTo("project")
7
+
8
+ export default ProjectDetail
@@ -4,6 +4,7 @@ class Project extends DatabaseRecord {
4
4
  }
5
5
 
6
6
  Project.hasMany("tasks")
7
+ Project.hasOne("projectDetail")
7
8
  Project.translates("name")
8
9
 
9
10
  export default Project
@@ -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 = () => this.localeFallbacks
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 = () => digg(this, "locales")
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 = (identifier = "default") => Boolean(this.databasePools[identifier])
119
- isInitialized = () => this._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 = () => digg(this, "data", "ParentColumn")
6
- getName = () => digg(this, "data", "CONSTRAINT_NAME")
7
- getTableName = () => digg(this, "data", "TableName")
8
- getReferencedColumnName = () => digg(this, "data", "ReferencedColumn")
9
- getReferencedTableName = () => digg(this, "data", "ReferencedTable")
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 = () => "mssql"
86
- primaryKeyType = () => "bigint"
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 = (string) => {
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 = (string) => {
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 = (string) => {
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 = () => digg(this, "data", "COLUMN_NAME")
6
- getName = () => digg(this, "data", "CONSTRAINT_NAME")
7
- getTableName = () => digg(this, "data", "TABLE_NAME")
8
- getReferencedColumnName = () => digg(this, "data", "REFERENCED_COLUMN_NAME")
9
- getReferencedTableName = () => digg(this, "data", "REFERENCED_TABLE_NAME")
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 = () => "mysql"
95
- primaryKeyType = () => "bigint"
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 = () => true
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 = () => digg(this, "data", "column_name")
6
- getName = () => digg(this, "data", "constraint_name")
7
- getTableName = () => digg(this, "data", "table_name")
8
- getReferencedColumnName = () => digg(this, "data", "foreign_column_name")
9
- getReferencedTableName = () => digg(this, "data", "foreign_table_name")
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 = () => "pgsql"
103
- primaryKeyType = () => "bigint"
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 = () => true
121
+ shouldSetAutoIncrementWhenPrimaryKey() { return true }
122
122
 
123
123
  escape(value) {
124
124
  if (!this.connection) throw new Error("Can't escape before connected")
@@ -10,7 +10,7 @@ export default class VelociousDatabasePoolAsyncTrackedMultiConnection extends Ba
10
10
  this.idSeq = 0
11
11
  }
12
12
 
13
- checkin = (connection) => {
13
+ checkin(connection) {
14
14
  const id = connection.getIdSeq()
15
15
 
16
16
  if (id in this.connectionsInUse) {
@@ -1,7 +1,7 @@
1
1
  import BasePool from "./base.js"
2
2
 
3
3
  export default class VelociousDatabasePoolSingleMultiUser extends BasePool {
4
- checkin = (connection) => {
4
+ checkin(connection) {
5
5
  // Do nothing
6
6
  }
7
7
 
@@ -6,5 +6,5 @@ export default class VelociousDatabaseQueryFromPlain extends FromBase {
6
6
  this.plain = plain
7
7
  }
8
8
 
9
- toSql = () => this.plain
9
+ toSql() { return this.plain }
10
10
  }
@@ -71,7 +71,7 @@ export default class VelociousDatabaseQuery {
71
71
  throw new Error("count multiple stub")
72
72
  }
73
73
 
74
- getOptions = () => this.driver.options()
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 = () => this.driver.queryToSql(this)
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 (modelClassRelationship.getType() == "belongsTo") {
155
+ if (relationshipType == "belongsTo") {
138
156
  instanceRelationship = new BelongsToInstanceRelationship({model: this, relationship: modelClassRelationship})
139
- } else if (modelClassRelationship.getType() == "hasMany") {
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: ${modelClassRelationship.getType()}`)
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._autoSaveHasManyRelationshipsToSave()
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._autoSaveHasManyRelationships({isNewRecord})
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
- _autoSaveHasManyRelationshipsToSave() {
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 = instanceRelationship._loaded
454
+ let loaded
431
455
 
432
- if (!Array.isArray(loaded)) loaded = [loaded]
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 _autoSaveHasManyRelationships({isNewRecord}) {
454
- for (const instanceRelationship of this._autoSaveHasManyRelationshipsToSave()) {
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 = () => this._dirty
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 = () => this.getRelationship().getForeignKey()
31
- getPrimaryKey = () => this.getRelationship().getPrimaryKey()
32
- getRelationship = () => this.relationship
33
- getTargetModelClass = () => this.getRelationship().getTargetModelClass()
34
- getType = () => this.getRelationship().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
  }
@@ -14,7 +14,7 @@ export default class VelociousDatabaseRecordBelongsToInstanceRelationship extend
14
14
  return newInstance
15
15
  }
16
16
 
17
- setLoaded(models) {
18
- this._loaded = models
17
+ getLoadedOrNull() {
18
+ return this._loaded
19
19
  }
20
20
  }
@@ -23,6 +23,10 @@ export default class VelociousDatabaseRecordHasManyInstanceRelationship extends
23
23
  return this._loaded
24
24
  }
25
25
 
26
+ getLoadedOrNull() {
27
+ return this._loaded
28
+ }
29
+
26
30
  addToLoaded(models) {
27
31
  if (Array.isArray(models)) {
28
32
  for (const model of models) {
@@ -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
- const restArgsKeys = Object.keys(restArgs)
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 = () => this._dependent
21
- getRelationshipName = () => this.relationshipName
22
- getPrimaryKey = () => "id" // TODO: Support custom given primary key
23
- getType = () => this.type
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
+ }