velocious 1.0.100 → 1.0.102

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 (38) hide show
  1. package/package.json +1 -1
  2. package/spec/dummy/src/model-bases/authentication-token.js +3 -4
  3. package/spec/dummy/src/model-bases/project-detail.js +3 -4
  4. package/spec/dummy/src/model-bases/project-translation.js +3 -4
  5. package/spec/dummy/src/model-bases/project.js +10 -16
  6. package/spec/dummy/src/model-bases/task.js +3 -4
  7. package/spec/dummy/src/model-bases/user.js +7 -12
  8. package/src/cli/base-command.js +1 -1
  9. package/src/configuration-types.js +7 -1
  10. package/src/configuration.js +2 -2
  11. package/src/database/drivers/base-column.js +8 -7
  12. package/src/database/drivers/base-columns-index.js +1 -1
  13. package/src/database/drivers/base-foreign-key.js +5 -5
  14. package/src/database/drivers/base-table.js +4 -4
  15. package/src/database/drivers/base.js +45 -23
  16. package/src/database/pool/base.js +5 -5
  17. package/src/database/query/base.js +1 -1
  18. package/src/database/query/from-base.js +2 -4
  19. package/src/database/query/index.js +31 -18
  20. package/src/database/query/order-base.js +1 -1
  21. package/src/database/query/select-base.js +1 -1
  22. package/src/database/query/where-base.js +1 -1
  23. package/src/database/record/index.js +444 -172
  24. package/src/database/record/instance-relationships/base.js +41 -44
  25. package/src/database/record/instance-relationships/belongs-to.js +15 -3
  26. package/src/database/record/instance-relationships/has-many.js +49 -28
  27. package/src/database/record/instance-relationships/has-one.js +22 -7
  28. package/src/database/record/relationships/base.js +35 -45
  29. package/src/database/record/relationships/belongs-to.js +13 -3
  30. package/src/database/record/relationships/has-many.js +8 -2
  31. package/src/database/record/relationships/has-one.js +8 -2
  32. package/src/database/record/validators/base.js +15 -3
  33. package/src/database/record/validators/presence.js +7 -0
  34. package/src/environment-handlers/base.js +7 -7
  35. package/src/environment-handlers/node/cli/commands/generate/base-models.js +5 -8
  36. package/src/environment-handlers/node.js +1 -2
  37. package/src/initializer.js +1 -1
  38. package/src/routes/base-route.js +1 -1
@@ -1,12 +1,18 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * @typedef {{type: string, message: string}} ValidationErrorObjectType
5
+ */
6
+
1
7
  import BelongsToInstanceRelationship from "./instance-relationships/belongs-to.js"
2
8
  import BelongsToRelationship from "./relationships/belongs-to.js"
3
9
  import Configuration from "../../configuration.js"
4
10
  import FromTable from "../query/from-table.js"
5
11
  import Handler from "../handler.js"
6
- import HasManyRelationship from "./relationships/has-many.js"
7
12
  import HasManyInstanceRelationship from "./instance-relationships/has-many.js"
8
- import HasOneRelationship from "./relationships/has-one.js"
13
+ import HasManyRelationship from "./relationships/has-many.js"
9
14
  import HasOneInstanceRelationship from "./instance-relationships/has-one.js"
15
+ import HasOneRelationship from "./relationships/has-one.js"
10
16
  import * as inflection from "inflection"
11
17
  import Query from "../query/index.js"
12
18
  import restArgsError from "../../utils/rest-args-error.js"
@@ -15,53 +21,120 @@ import ValidatorsUniqueness from "./validators/uniqueness.js"
15
21
 
16
22
  class ValidationError extends Error {
17
23
  /**
18
- * @template T extends VelociousDatabaseRecord
19
- * @returns {T}
24
+ * @returns {VelociousDatabaseRecord}
20
25
  */
21
26
  getModel() {
27
+ if (!this._model) throw new Error("Model hasn't been set")
28
+
22
29
  return this._model
23
30
  }
24
31
 
25
32
  /**
26
- * @template T extends VelociousDatabaseRecord
27
- * @param {T} model
33
+ * @param {VelociousDatabaseRecord} model
28
34
  */
29
35
  setModel(model) {
30
36
  this._model = model
31
37
  }
32
38
 
33
39
  /**
34
- * @returns {Array}
40
+ * @returns {Record<string, ValidationErrorObjectType[]>}
35
41
  */
36
42
  getValidationErrors() {
43
+ if (!this._validationErrors) throw new Error("Validation errors hasn't been set")
44
+
37
45
  return this._validationErrors
38
46
  }
39
47
 
48
+ /**
49
+ * @param {Record<string, ValidationErrorObjectType[]>} validationErrors
50
+ */
40
51
  setValidationErrors(validationErrors) {
41
52
  this._validationErrors = validationErrors
42
53
  }
43
54
  }
44
55
 
45
56
  class VelociousDatabaseRecord {
46
- /**
47
- * @returns {Record<string, import("./validators/base.js").default>}
48
- */
49
- static validatorTypes() {
50
- if (!this._validatorTypes) this._validatorTypes = {}
57
+ static getAttributeNameToColumnNameMap() {
58
+ if (!this._attributeNameToColumnName) {
59
+ /** @type {Record<string, string>} */
60
+ this._attributeNameToColumnName = {}
61
+ }
62
+
63
+ return this._attributeNameToColumnName
64
+ }
65
+
66
+ static getColumnNameToAttributeNameMap() {
67
+ if (!this._columnNameToAttributeName) {
68
+ /** @type {Record<string, string>} */
69
+ this._columnNameToAttributeName = {}
70
+ }
71
+
72
+ return this._columnNameToAttributeName
73
+ }
74
+
75
+ static getTranslationsMap() {
76
+ if (!this._translations) {
77
+ /** @type {Record<string, object>} */
78
+ this._translations = {}
79
+ }
80
+
81
+ return this._translations
82
+ }
83
+
84
+ static getValidatorsMap() {
85
+ if (!this._validators) {
86
+ /** @type {Record<string, import("./validators/base.js").default[]>} */
87
+ this._validators = {}
88
+ }
89
+
90
+ return this._validators
91
+ }
92
+
93
+ static getValidatorTypesMap() {
94
+ if (!this._validatorTypes) {
95
+ /** @type {Record<string, typeof import("./validators/base.js").default>} */
96
+ this._validatorTypes = {}
97
+ }
51
98
 
52
99
  return this._validatorTypes
53
100
  }
54
101
 
102
+ /** @type {Record<string, any>} */
103
+ _attributes = {}
104
+
105
+ /** @type {Record<string, any>} */
106
+ _changes = {}
107
+
108
+ /** @type {Record<string, import("../drivers/base-column.js").default>} */
109
+ _columnsAsHash = {}
110
+
111
+ /** @type {import("../drivers/base.js").default | undefined} */
112
+ __connection = undefined
113
+
114
+ /** @type {Record<string, import("./instance-relationships/base.js").default>} */
115
+ _instanceRelationships = {}
116
+
117
+ /** @type {string | undefined} */
118
+ __tableName = undefined
119
+
120
+ /** @type {Record<string, ValidationErrorObjectType[]>} */
121
+ _validationErrors = {}
122
+
123
+ static validatorTypes() {
124
+ return this.getValidatorTypesMap()
125
+ }
126
+
55
127
  /**
56
128
  * @param {string} name
57
- * @param {import("./validators/base.js").default} validatorClass
129
+ * @param {typeof import("./validators/base.js").default} validatorClass
58
130
  */
59
131
  static registerValidatorType(name, validatorClass) {
60
132
  this.validatorTypes()[name] = validatorClass
61
133
  }
62
134
 
63
135
  /**
64
- * @returns {import("./validators/base.js").default}
136
+ * @param {string} validatorName
137
+ * @returns {typeof import("./validators/base.js").default}
65
138
  */
66
139
  static getValidatorType(validatorName) {
67
140
  if (!(validatorName in this.validatorTypes())) throw new Error(`Validator type ${validatorName} not found`)
@@ -70,19 +143,29 @@ class VelociousDatabaseRecord {
70
143
  }
71
144
 
72
145
  /**
146
+ * @param {string} relationshipName
73
147
  * @returns {boolean}
74
148
  */
75
149
  static _relationshipExists(relationshipName) {
76
- if (this._relationships && relationshipName in this._relationships) {
150
+ if (relationshipName in this.getRelationshipsMap()) {
77
151
  return true
78
152
  }
79
153
 
80
154
  return false
81
155
  }
82
156
 
157
+ /**
158
+ * @typedef {object} RelationshipDataArgumentType
159
+ * @property {string} [className]
160
+ * @property {typeof VelociousDatabaseRecord} [klass]
161
+ * @property {string} [type]
162
+ */
163
+ /**
164
+ * @param {string} relationshipName
165
+ * @param {RelationshipDataArgumentType} data
166
+ */
83
167
  static _defineRelationship(relationshipName, data) {
84
168
  if (!relationshipName) throw new Error(`Invalid relationship name given: ${relationshipName}`)
85
- if (!this._relationships) this._relationships = {}
86
169
  if (this._relationshipExists(relationshipName)) throw new Error(`Relationship ${relationshipName} already exists`)
87
170
 
88
171
  const actualData = Object.assign(
@@ -98,21 +181,29 @@ class VelociousDatabaseRecord {
98
181
  actualData.className = inflection.camelize(inflection.singularize(relationshipName))
99
182
  }
100
183
 
184
+ /** @type {Record<string, (this: VelociousDatabaseRecord) => unknown>} */
185
+ const proto = /** @type {any} */ (this.prototype);
186
+
101
187
  let relationship
102
188
 
103
189
  if (actualData.type == "belongsTo") {
104
190
  relationship = new BelongsToRelationship(actualData)
105
191
 
106
- this.prototype[relationshipName] = function() {
192
+ proto[relationshipName] = function() {
107
193
  const relationship = this.getRelationshipByName(relationshipName)
108
194
 
109
195
  return relationship.loaded()
110
196
  }
111
197
 
112
- this.prototype[`build${inflection.camelize(relationshipName)}`] = function(attributes) {
198
+ // @ts-expect-error
199
+ proto[`build${inflection.camelize(relationshipName)}`] = function(attributes) {
200
+ // @ts-expect-error
113
201
  const instanceRelationship = this.getRelationshipByName(relationshipName)
202
+
203
+ // @ts-expect-error
114
204
  const record = instanceRelationship.build(attributes)
115
205
 
206
+ // @ts-expect-error
116
207
  const inverseOf = instanceRelationship.getRelationship().getInverseOf()
117
208
 
118
209
  if (inverseOf) {
@@ -132,41 +223,51 @@ class VelociousDatabaseRecord {
132
223
  return record
133
224
  }
134
225
 
135
- this.prototype[`load${inflection.camelize(relationshipName)}`] = async function() {
226
+ proto[`load${inflection.camelize(relationshipName)}`] = async function() {
136
227
  await this.getRelationshipByName(relationshipName).load()
137
228
  }
138
229
 
139
- this.prototype[`set${inflection.camelize(relationshipName)}`] = function(model) {
230
+ // @ts-expect-error
231
+ proto[`set${inflection.camelize(relationshipName)}`] = function(model) {
232
+ // @ts-expect-error
140
233
  const relationship = this.getRelationshipByName(relationshipName)
141
234
 
235
+ // @ts-expect-error
142
236
  relationship.setLoaded(model)
237
+
238
+ // @ts-expect-error
143
239
  relationship.setDirty(true)
144
240
  }
145
241
  } else if (actualData.type == "hasMany") {
146
242
  relationship = new HasManyRelationship(actualData)
147
243
 
148
- this.prototype[relationshipName] = function() {
244
+ proto[relationshipName] = function() {
149
245
  return this.getRelationshipByName(relationshipName)
150
246
  }
151
247
 
152
- this.prototype[`${relationshipName}Loaded`] = function() {
248
+ proto[`${relationshipName}Loaded`] = function() {
153
249
  return this.getRelationshipByName(relationshipName).loaded()
154
250
  }
155
251
 
156
- this.prototype[`load${inflection.camelize(relationshipName)}`] = async function() {
252
+ proto[`load${inflection.camelize(relationshipName)}`] = async function() {
157
253
  await this.getRelationshipByName(relationshipName).load()
158
254
  }
159
255
  } else if (actualData.type == "hasOne") {
160
256
  relationship = new HasOneRelationship(actualData)
161
257
 
162
- this.prototype[relationshipName] = function() {
258
+ proto[relationshipName] = function() {
163
259
  return this.getRelationshipByName(relationshipName).loaded()
164
260
  }
165
261
 
166
- this.prototype[`build${inflection.camelize(relationshipName)}`] = function(attributes) {
262
+ // @ts-expect-error
263
+ proto[`build${inflection.camelize(relationshipName)}`] = function(attributes) {
264
+ // @ts-expect-error
167
265
  const instanceRelationship = this.getRelationshipByName(relationshipName)
266
+
267
+ // @ts-expect-error
168
268
  const record = instanceRelationship.build(attributes)
169
269
 
270
+ // @ts-expect-error
170
271
  const inverseOf = instanceRelationship.getRelationship().getInverseOf()
171
272
 
172
273
  if (inverseOf) {
@@ -179,23 +280,22 @@ class VelociousDatabaseRecord {
179
280
  return record
180
281
  }
181
282
 
182
- this.prototype[`load${inflection.camelize(relationshipName)}`] = async function() {
283
+ proto[`load${inflection.camelize(relationshipName)}`] = async function() {
183
284
  await this.getRelationshipByName(relationshipName).load()
184
285
  }
185
286
  } else {
186
287
  throw new Error(`Unknown relationship type: ${actualData.type}`)
187
288
  }
188
289
 
189
- this._relationships[relationshipName] = relationship
290
+ this.getRelationshipsMap()[relationshipName] = relationship
190
291
  }
191
292
 
192
293
  /**
294
+ * @param {string} relationshipName
193
295
  * @returns {import("./relationships/base.js").default}
194
296
  */
195
297
  static getRelationshipByName(relationshipName) {
196
- if (!this._relationships) this._relationships = {}
197
-
198
- const relationship = this._relationships[relationshipName]
298
+ const relationship = this.getRelationshipsMap()[relationshipName]
199
299
 
200
300
  if (!relationship) throw new Error(`No relationship by that name: ${relationshipName}`)
201
301
 
@@ -206,9 +306,16 @@ class VelociousDatabaseRecord {
206
306
  * @returns {Array<import("./relationships/base.js").default>}
207
307
  */
208
308
  static getRelationships() {
209
- if (this._relationships) return Object.values(this._relationships)
309
+ return Object.values(this.getRelationshipsMap())
310
+ }
210
311
 
211
- return []
312
+ static getRelationshipsMap() {
313
+ if (!this._relationships) {
314
+ /** @type {Record<string, import("./relationships/base.js").default>} */
315
+ this._relationships = {}
316
+ }
317
+
318
+ return this._relationships
212
319
  }
213
320
 
214
321
  /**
@@ -219,14 +326,12 @@ class VelociousDatabaseRecord {
219
326
  }
220
327
 
221
328
  /**
222
- * @template T extends import("./instance-relationships/index.js").default
223
- * @returns {T}
329
+ * @param {string} relationshipName
330
+ * @returns {import("./instance-relationships/base.js").default}
224
331
  */
225
332
  getRelationshipByName(relationshipName) {
226
- if (!this._instanceRelationships) this._instanceRelationships = {}
227
-
228
333
  if (!(relationshipName in this._instanceRelationships)) {
229
- const modelClassRelationship = this.constructor.getRelationshipByName(relationshipName)
334
+ const modelClassRelationship = this.getModelClass().getRelationshipByName(relationshipName)
230
335
  const relationshipType = modelClassRelationship.getType()
231
336
  let instanceRelationship
232
337
 
@@ -249,15 +354,14 @@ class VelociousDatabaseRecord {
249
354
  /**
250
355
  * Adds a belongs-to-relationship to the model.
251
356
  * @param {string} relationshipName The name of the relationship.
252
- * @param {object} options The options for the relationship.
357
+ * @param {object} [options] The options for the relationship.
253
358
  */
254
359
  static belongsTo(relationshipName, options) {
255
360
  this._defineRelationship(relationshipName, Object.assign({type: "belongsTo"}, options))
256
361
  }
257
362
 
258
363
  /**
259
- * @template T extends import("./database/drivers/base").default
260
- * @returns {T}
364
+ * @returns {import("../drivers/base.js").default}
261
365
  */
262
366
  static connection() {
263
367
  const databasePool = this._getConfiguration().getDatabasePool(this.getDatabaseIdentifier())
@@ -299,7 +403,7 @@ class VelociousDatabaseRecord {
299
403
  * @returns {import("../../configuration.js").default}
300
404
  */
301
405
  _getConfiguration() {
302
- return this.constructor._getConfiguration()
406
+ return this.getModelClass()._getConfiguration()
303
407
  }
304
408
 
305
409
  /**
@@ -335,12 +439,16 @@ class VelociousDatabaseRecord {
335
439
  /**
336
440
  * @returns {string}
337
441
  */
338
- static getDatabaseType() { return this._databaseType }
442
+ static getDatabaseType() {
443
+ if (!this._databaseType) throw new Error("Database type hasn't been set")
444
+
445
+ return this._databaseType
446
+ }
339
447
 
340
448
  /**
341
449
  * @param {object} args
342
- * @param {import("../configuration.js").default} args.configuration
343
- * @returns {void}
450
+ * @param {import("../../configuration.js").default} args.configuration
451
+ * @returns {Promise<void>}
344
452
  */
345
453
  static async initializeRecord({configuration, ...restArgs}) {
346
454
  restArgsError(restArgs)
@@ -353,9 +461,15 @@ class VelociousDatabaseRecord {
353
461
 
354
462
  this._table = await this.connection().getTableByName(this.tableName())
355
463
  this._columns = await this._getTable().getColumns()
464
+
465
+ /** @type {Record<string, import("../drivers/base-column.js").default>} */
356
466
  this._columnsAsHash = {}
357
- this._columnNameToAttributeName = {}
358
- this._attributeNameToColumnName = {}
467
+
468
+ const columnNameToAttributeName = this.getColumnNameToAttributeNameMap()
469
+ const attributeNameToColumnName = this.getAttributeNameToColumnNameMap()
470
+
471
+ /** @type {Record<string, (this: VelociousDatabaseRecord) => unknown>} */
472
+ const proto = /** @type {any} */ (this.prototype);
359
473
 
360
474
  for (const column of this._columns) {
361
475
  this._columnsAsHash[column.getName()] = column
@@ -363,18 +477,21 @@ class VelociousDatabaseRecord {
363
477
  const camelizedColumnName = inflection.camelize(column.getName(), true)
364
478
  const camelizedColumnNameBigFirst = inflection.camelize(column.getName())
365
479
 
366
- this._attributeNameToColumnName[camelizedColumnName] = column.getName()
367
- this._columnNameToAttributeName[column.getName()] = camelizedColumnName
480
+ attributeNameToColumnName[camelizedColumnName] = column.getName()
481
+ columnNameToAttributeName[column.getName()] = camelizedColumnName
368
482
 
369
- this.prototype[camelizedColumnName] = function() {
483
+ proto[camelizedColumnName] = function() {
370
484
  return this.readAttribute(camelizedColumnName)
371
485
  }
372
486
 
373
- this.prototype[`set${camelizedColumnNameBigFirst}`] = function(newValue) {
487
+ // @ts-expect-error
488
+ proto[`set${camelizedColumnNameBigFirst}`] = function(newValue) {
489
+ // @ts-expect-error
374
490
  return this._setColumnAttribute(camelizedColumnName, newValue)
375
491
  }
376
492
 
377
- this.prototype[`has${camelizedColumnNameBigFirst}`] = function() {
493
+ proto[`has${camelizedColumnNameBigFirst}`] = function() {
494
+ // @ts-expect-error
378
495
  let value = this[camelizedColumnName]()
379
496
 
380
497
  return this._hasAttribute(value)
@@ -386,6 +503,7 @@ class VelociousDatabaseRecord {
386
503
  }
387
504
 
388
505
  /**
506
+ * @param {any} value
389
507
  * @returns {boolean}
390
508
  */
391
509
  _hasAttribute(value) {
@@ -421,21 +539,37 @@ class VelociousDatabaseRecord {
421
539
  const nameCamelized = inflection.camelize(name)
422
540
  const setterMethodName = `set${nameCamelized}`
423
541
 
424
- this.prototype[name] = function getTranslatedAttribute() {
542
+ /** @type {Record<string, unknown>} */
543
+ // @ts-expect-error
544
+ const self = this
545
+
546
+ /** @type {Record<string, (this: VelociousDatabaseRecord) => unknown>} */
547
+ const proto = /** @type {any} */ (this.prototype);
548
+
549
+ proto[name] = function getTranslatedAttribute() {
425
550
  const locale = this._getConfiguration().getLocale()
426
551
 
427
552
  return this._getTranslatedAttributeWithFallback(name, locale)
428
553
  }
429
554
 
430
- this.prototype[`has${nameCamelized}`] = function hasTranslatedAttribute() {
431
- const value = this[name]()
555
+ proto[`has${nameCamelized}`] = function hasTranslatedAttribute() {
556
+ const candidate = self[name]
432
557
 
433
- return this._hasAttribute(value)
558
+ if (typeof candidate == "function") {
559
+ const value = candidate()
560
+
561
+ return this._hasAttribute(value)
562
+ } else {
563
+ throw new Error(`Expected candidate to be a function but it was: ${typeof candidate}`)
564
+ }
434
565
  }
435
566
 
436
- this.prototype[setterMethodName] = function setTranslatedAttribute(newValue) {
567
+ // @ts-expect-error
568
+ proto[setterMethodName] = function setTranslatedAttribute(newValue) {
569
+ // @ts-expect-error
437
570
  const locale = this._getConfiguration().getLocale()
438
571
 
572
+ // @ts-expect-error
439
573
  return this._setTranslatedAttribute(name, locale, newValue)
440
574
  }
441
575
 
@@ -444,10 +578,12 @@ class VelociousDatabaseRecord {
444
578
  const getterMethodNameLocalized = `${name}${localeCamelized}`
445
579
  const setterMethodNameLocalized = `${setterMethodName}${localeCamelized}`
446
580
 
581
+ // @ts-expect-error
447
582
  this.prototype[getterMethodNameLocalized] = function getTranslatedAttributeWithLocale() {
448
583
  return this._getTranslatedAttribute(name, locale)
449
584
  }
450
585
 
586
+ // @ts-expect-error
451
587
  this.prototype[setterMethodNameLocalized] = function setTranslatedAttributeWithLocale(newValue) {
452
588
  return this._setTranslatedAttribute(name, locale, newValue)
453
589
  }
@@ -485,6 +621,16 @@ class VelociousDatabaseRecord {
485
621
  return this._attributes[columnName]
486
622
  }
487
623
 
624
+ /**
625
+ *
626
+ * @returns {typeof VelociousDatabaseRecord}
627
+ */
628
+ getModelClass() {
629
+ const modelClass = /** @type {typeof VelociousDatabaseRecord} */ (this.constructor)
630
+
631
+ return modelClass
632
+ }
633
+
488
634
  /**
489
635
  * @param {string} name
490
636
  * @param {*} newValue
@@ -493,16 +639,21 @@ class VelociousDatabaseRecord {
493
639
  setAttribute(name, newValue) {
494
640
  const setterName = `set${inflection.camelize(name)}`
495
641
 
496
- if (!this.constructor.isInitialized()) throw new Error(`${this.constructor.name} model isn't initialized yet`)
642
+ if (!this.getModelClass().isInitialized()) throw new Error(`${this.constructor.name} model isn't initialized yet`)
497
643
  if (!(setterName in this)) throw new Error(`No such setter method: ${this.constructor.name}#${setterName}`)
498
644
 
645
+ // @ts-expect-error
499
646
  this[setterName](newValue)
500
647
  }
501
648
 
649
+ /**
650
+ * @param {string} name
651
+ * @param {any} newValue
652
+ */
502
653
  _setColumnAttribute(name, newValue) {
503
- if (!this.constructor._attributeNameToColumnName) throw new Error("No attribute-to-column mapping. Has record been initialized?")
654
+ if (!this.getModelClass()._attributeNameToColumnName) throw new Error("No attribute-to-column mapping. Has record been initialized?")
504
655
 
505
- const columnName = this.constructor._attributeNameToColumnName[name]
656
+ const columnName = this.getModelClass().getAttributeNameToColumnNameMap()[name]
506
657
 
507
658
  if (!columnName) throw new Error(`Couldn't figure out column name for attribute: ${name}`)
508
659
 
@@ -543,14 +694,14 @@ class VelociousDatabaseRecord {
543
694
  /**
544
695
  * @param {Array<string>} columns
545
696
  * @param {Array<Array<string>>} rows
546
- * @returns {void}
697
+ * @returns {Promise<void>}
547
698
  */
548
699
  static async insertMultiple(columns, rows) {
549
700
  return await this.connection().insertMultiple(this.tableName(), columns, rows)
550
701
  }
551
702
 
552
703
  /**
553
- * @returns {number}
704
+ * @returns {Promise<number>}
554
705
  */
555
706
  static async nextPrimaryKey() {
556
707
  const primaryKey = this.primaryKey()
@@ -559,7 +710,13 @@ class VelociousDatabaseRecord {
559
710
  const newestRecord = await this.order(`${connection.quoteTable(tableName)}.${connection.quoteColumn(primaryKey)}`).last()
560
711
 
561
712
  if (newestRecord) {
562
- return newestRecord.id() + 1
713
+ const id = newestRecord.id()
714
+
715
+ if (typeof id == "number") {
716
+ return id + 1
717
+ } else {
718
+ throw new Error("ID from newest record wasn't a number")
719
+ }
563
720
  } else {
564
721
  return 1
565
722
  }
@@ -583,16 +740,16 @@ class VelociousDatabaseRecord {
583
740
  }
584
741
 
585
742
  /**
586
- * @returns {boolean}
743
+ * @returns {Promise<void>}
587
744
  */
588
745
  async save() {
589
746
  const isNewRecord = this.isNewRecord()
590
747
  let result
591
748
 
592
- await this.constructor._getConfiguration().ensureConnections(async () => {
749
+ await this._getConfiguration().ensureConnections(async () => {
593
750
  await this._runValidations()
594
751
 
595
- await this.constructor.transaction(async () => {
752
+ await this.getModelClass().transaction(async () => {
596
753
  // If any belongs-to-relationships was saved, then updated-at should still be set on this record.
597
754
  const {savedCount} = await this._autoSaveBelongsToRelationships()
598
755
 
@@ -630,17 +787,23 @@ class VelociousDatabaseRecord {
630
787
 
631
788
  const model = instanceRelationship.loaded()
632
789
 
633
- if (model?.isChanged()) {
634
- await model.save()
790
+ if (model) {
791
+ if (model instanceof VelociousDatabaseRecord) {
792
+ if (model.isChanged()) {
793
+ await model.save()
635
794
 
636
- const foreignKey = instanceRelationship.getForeignKey()
795
+ const foreignKey = instanceRelationship.getForeignKey()
637
796
 
638
- this.setAttribute(foreignKey, model.id())
797
+ this.setAttribute(foreignKey, model.id())
639
798
 
640
- instanceRelationship.setPreloaded(true)
641
- instanceRelationship.setDirty(false)
799
+ instanceRelationship.setPreloaded(true)
800
+ instanceRelationship.setDirty(false)
642
801
 
643
- savedCount++
802
+ savedCount++
803
+ }
804
+ } else {
805
+ throw new Error(`Expected a record but got: ${typeof model}`)
806
+ }
644
807
  }
645
808
  }
646
809
 
@@ -661,18 +824,21 @@ class VelociousDatabaseRecord {
661
824
  continue
662
825
  }
663
826
 
827
+ /** @type {VelociousDatabaseRecord[]} */
664
828
  let loaded
665
829
 
666
- if (instanceRelationship.getType() == "hasOne") {
667
- const hasOneLoaded = instanceRelationship.getLoadedOrNull()
830
+ const hasManyOrOneLoaded = instanceRelationship.getLoadedOrUndefined()
668
831
 
669
- if (hasOneLoaded) {
670
- loaded = [hasOneLoaded]
832
+ if (hasManyOrOneLoaded) {
833
+ if (Array.isArray(hasManyOrOneLoaded)) {
834
+ loaded = hasManyOrOneLoaded
835
+ } else if (hasManyOrOneLoaded instanceof VelociousDatabaseRecord) {
836
+ loaded = [hasManyOrOneLoaded]
671
837
  } else {
672
- continue
838
+ throw new Error(`Expected hasOneLoaded to be a record but it wasn't: ${typeof hasManyOrOneLoaded}`)
673
839
  }
674
840
  } else {
675
- loaded = instanceRelationship.getLoadedOrNull()
841
+ continue
676
842
  }
677
843
 
678
844
  let useRelationship = false
@@ -696,11 +862,26 @@ class VelociousDatabaseRecord {
696
862
  return relationships
697
863
  }
698
864
 
865
+ /**
866
+ * @param {object} args
867
+ * @param {boolean} args.isNewRecord
868
+ */
699
869
  async _autoSaveHasManyAndHasOneRelationships({isNewRecord}) {
700
870
  for (const instanceRelationship of this._autoSaveHasManyAndHasOneRelationshipsToSave()) {
701
- let loaded = instanceRelationship.getLoadedOrNull()
871
+ let hasManyOrOneLoaded = instanceRelationship.getLoadedOrUndefined()
702
872
 
703
- if (!Array.isArray(loaded)) loaded = [loaded]
873
+ /** @type {VelociousDatabaseRecord[]} */
874
+ let loaded
875
+
876
+ if (hasManyOrOneLoaded === undefined) {
877
+ loaded = []
878
+ } else if (hasManyOrOneLoaded instanceof VelociousDatabaseRecord) {
879
+ loaded = [hasManyOrOneLoaded]
880
+ } else if (Array.isArray(hasManyOrOneLoaded)) {
881
+ loaded = hasManyOrOneLoaded
882
+ } else {
883
+ throw new Error(`Unexpected type for hasManyOrOneLoaded: ${typeof hasManyOrOneLoaded}`)
884
+ }
704
885
 
705
886
  for (const model of loaded) {
706
887
  const foreignKey = instanceRelationship.getForeignKey()
@@ -736,8 +917,8 @@ class VelociousDatabaseRecord {
736
917
  }
737
918
 
738
919
  /**
739
- * @param {function() : void} callback
740
- * @returns {*}
920
+ * @param {function() : Promise<void>} callback
921
+ * @returns {Promise<*>}
741
922
  */
742
923
  static async transaction(callback) {
743
924
  const useTransactions = this.connection().getArgs().record?.transactions
@@ -750,14 +931,16 @@ class VelociousDatabaseRecord {
750
931
  }
751
932
 
752
933
  /**
934
+ * @param {...string} names
753
935
  * @returns {void}
754
936
  */
755
937
  static translates(...names) {
938
+ const translations = this.getTranslationsMap()
939
+
756
940
  for (const name of names) {
757
- if (!this._translations) this._translations = {}
758
- if (name in this._translations) throw new Error(`Translation already exists: ${name}`)
941
+ if (name in translations) throw new Error(`Translation already exists: ${name}`)
759
942
 
760
- this._translations[name] = {}
943
+ translations[name] = {}
761
944
 
762
945
  if (!this._relationshipExists("translations")) {
763
946
  this._defineRelationship("translations", {klass: this.getTranslationClass(), type: "hasMany"})
@@ -812,11 +995,33 @@ class VelociousDatabaseRecord {
812
995
  /**
813
996
  * Adds a validation to an attribute.
814
997
  * @param {string} attributeName The name of the attribute to validate.
815
- * @param {object} validators The validators to add. Key is the validator name, value is the validator arguments.
998
+ * @param {Record<string, boolean | Record<string, any>>} validators The validators to add. Key is the validator name, value is the validator arguments.
816
999
  */
817
1000
  static async validates(attributeName, validators) {
818
1001
  for (const validatorName in validators) {
819
- const validatorArgs = validators[validatorName]
1002
+ /** @type {Record<string, any>} */
1003
+ let validatorArgs
1004
+
1005
+ /** @type {boolean} */
1006
+ let useValidator = true
1007
+
1008
+ const validatorArgsCandidate = validators[validatorName]
1009
+
1010
+ if (typeof validatorArgsCandidate == "boolean") {
1011
+ validatorArgs = {}
1012
+ useValidator
1013
+
1014
+ if (!validatorArgsCandidate) {
1015
+ useValidator = false
1016
+ }
1017
+ } else {
1018
+ validatorArgs = validatorArgsCandidate
1019
+ }
1020
+
1021
+ if (!useValidator) {
1022
+ continue
1023
+ }
1024
+
820
1025
  const ValidatorClass = this.getValidatorType(validatorName)
821
1026
  const validator = new ValidatorClass({attributeName, args: validatorArgs})
822
1027
 
@@ -827,16 +1032,34 @@ class VelociousDatabaseRecord {
827
1032
  }
828
1033
  }
829
1034
 
1035
+ /**
1036
+ * @abstract
1037
+ * @returns {TranslationBase[]}
1038
+ */
1039
+ translationsLoaded() {
1040
+ throw new Error("'translationsLoaded' not implemented")
1041
+ }
1042
+
830
1043
  /**
831
1044
  * @param {string} name
832
1045
  * @param {string} locale
833
1046
  * @returns {*}
834
1047
  */
835
1048
  _getTranslatedAttribute(name, locale) {
836
- const translation = this.translations().loaded().find((translation) => translation.locale() == locale)
1049
+ const translation = this.translationsLoaded().find((translation) => translation.locale() == locale)
837
1050
 
838
1051
  if (translation) {
839
- return translation[name]()
1052
+ /** @type {Record<string, unknown>} */
1053
+ // @ts-expect-error
1054
+ const dict = translation
1055
+
1056
+ const attributeMethod = /** @type {function() : any | undefined} */ (dict[name])
1057
+
1058
+ if (typeof attributeMethod == "function") {
1059
+ return attributeMethod.bind(translation)()
1060
+ } else {
1061
+ throw new Error(`No such translated method: ${name} (${typeof attributeMethod})`)
1062
+ }
840
1063
  }
841
1064
  }
842
1065
 
@@ -871,12 +1094,18 @@ class VelociousDatabaseRecord {
871
1094
  * @returns {void}
872
1095
  */
873
1096
  _setTranslatedAttribute(name, locale, newValue) {
874
- let translation = this.translations().loaded()?.find((translation) => translation.locale() == locale)
1097
+ /** @type {VelociousDatabaseRecord | TranslationBase | undefined} */
1098
+ let translation
1099
+
1100
+ translation = this.translationsLoaded()?.find((translation) => translation.locale() == locale)
875
1101
 
876
1102
  if (!translation) {
877
- translation = this.translations().build({locale})
1103
+ const instanceRelationship = this.getRelationshipByName("translations")
1104
+
1105
+ translation = instanceRelationship.build({locale})
878
1106
  }
879
1107
 
1108
+ /** @type {Record<string, any>} */
880
1109
  const assignments = {}
881
1110
 
882
1111
  assignments[name] = newValue
@@ -915,54 +1144,56 @@ class VelociousDatabaseRecord {
915
1144
  }
916
1145
 
917
1146
  /**
918
- * @returns {number}
1147
+ * @returns {Promise<number>}
919
1148
  */
920
1149
  static async count() {
921
1150
  return await this._newQuery().count()
922
1151
  }
923
1152
 
924
- static async destroyAll(...args) {
925
- return await this._newQuery().destroyAll(...args)
1153
+ static async destroyAll() {
1154
+ return await this._newQuery().destroyAll()
926
1155
  }
927
1156
 
928
1157
  /**
929
- * @param {...Parameters<Query["find"]>} args
1158
+ * @param {number|string} recordId
930
1159
  * @returns {Promise<InstanceType<typeof this>>}
931
1160
  */
932
- static async find(...args) {
933
- return await this._newQuery().find(...args)
1161
+ static async find(recordId) {
1162
+ return await this._newQuery().find(recordId)
934
1163
  }
935
1164
 
936
1165
  /**
937
- * @param {...Parameters<Query["findBy"]>} args
938
- * @returns {Promise<InstanceType<typeof this>>}
1166
+ * @param {{[key: string]: any}} conditions
1167
+ * @returns {Promise<InstanceType<typeof this> | null>}
939
1168
  */
940
- static async findBy(...args) {
941
- return await this._newQuery().findBy(...args)
1169
+ static async findBy(conditions) {
1170
+ return await this._newQuery().findBy(conditions)
942
1171
  }
943
1172
 
944
1173
  /**
945
- * @param {...Parameters<Query["findByOrFail"]>} args
1174
+ * @param {{[key: string]: any}} conditions
946
1175
  * @returns {Promise<InstanceType<typeof this>>}
947
1176
  */
948
- static async findByOrFail(...args) {
949
- return await this._newQuery().findByOrFail(...args)
1177
+ static async findByOrFail(conditions) {
1178
+ return await this._newQuery().findByOrFail(conditions)
950
1179
  }
951
1180
 
952
1181
  /**
953
- * @param {...Parameters<Query["findOrCreateBy"]>} args
1182
+ * @param {{[key: string]: any}} conditions
1183
+ * @param {function() : void} callback
954
1184
  * @returns {Promise<InstanceType<typeof this>>}
955
1185
  */
956
- static async findOrCreateBy(...args) {
957
- return await this._newQuery().findOrCreateBy(...args)
1186
+ static async findOrCreateBy(conditions, callback) {
1187
+ return await this._newQuery().findOrCreateBy(conditions, callback)
958
1188
  }
959
1189
 
960
1190
  /**
961
- * @param {...Parameters<Query["findOrInitializeBy"]>} args
1191
+ * @param {object} conditions
1192
+ * @param {function(import("../record/index.js").default) : void} callback
962
1193
  * @returns {Promise<InstanceType<typeof this>>}
963
1194
  */
964
- static async findOrInitializeBy(...args) {
965
- return await this._newQuery().findOrInitializeBy(...args)
1195
+ static async findOrInitializeBy(conditions, callback) {
1196
+ return await this._newQuery().findOrInitializeBy(conditions, callback)
966
1197
  }
967
1198
 
968
1199
  /**
@@ -973,69 +1204,74 @@ class VelociousDatabaseRecord {
973
1204
  }
974
1205
 
975
1206
  /**
1207
+ * @param {string|{[key: string]: any}} join
976
1208
  * @returns {Query}
977
1209
  */
978
- static joins(...args) {
979
- return this._newQuery().joins(...args)
1210
+ static joins(join) {
1211
+ return this._newQuery().joins(join)
980
1212
  }
981
1213
 
982
1214
  /**
983
1215
  * @returns {Promise<InstanceType<typeof this>>}
984
1216
  */
985
- static async last(...args) {
986
- return await this._newQuery().last(...args)
1217
+ static async last() {
1218
+ return await this._newQuery().last()
987
1219
  }
988
1220
 
989
1221
  /**
1222
+ * @param {number} value
990
1223
  * @returns {Query}
991
1224
  */
992
- static limit(...args) {
993
- return this._newQuery().limit(...args)
1225
+ static limit(value) {
1226
+ return this._newQuery().limit(value)
994
1227
  }
995
1228
 
996
1229
  /**
1230
+ * @param {string | number} order
997
1231
  * @returns {Query}
998
1232
  */
999
- static order(...args) {
1000
- return this._newQuery().order(...args)
1233
+ static order(order) {
1234
+ return this._newQuery().order(order)
1001
1235
  }
1002
1236
 
1003
1237
  /**
1238
+ * @param {import("../query/index.js").NestedPreloadRecord} preload
1004
1239
  * @returns {Query}
1005
1240
  */
1006
- static preload(...args) {
1007
- return this._newQuery().preload(...args)
1241
+ static preload(preload) {
1242
+ return this._newQuery().preload(preload)
1008
1243
  }
1009
1244
 
1010
1245
  /**
1246
+ * @param {import("../query/index.js").SelectArgumentType} select
1011
1247
  * @returns {Query}
1012
1248
  */
1013
- static select(...args) {
1014
- return this._newQuery().select(...args)
1249
+ static select(select) {
1250
+ return this._newQuery().select(select)
1015
1251
  }
1016
1252
 
1017
1253
  /**
1018
- * @returns {Query}
1254
+ * @returns {Promise<VelociousDatabaseRecord[]>}
1019
1255
  */
1020
- static toArray(...args) {
1021
- return this._newQuery().toArray(...args)
1256
+ static toArray() {
1257
+ return this._newQuery().toArray()
1022
1258
  }
1023
1259
 
1024
1260
  /**
1261
+ * @param {import("../query/index.js").WhereArgumentType} where
1025
1262
  * @returns {Query}
1026
1263
  */
1027
- static where(...args) {
1028
- return this._newQuery().where(...args)
1264
+ static where(where) {
1265
+ return this._newQuery().where(where)
1029
1266
  }
1030
1267
 
1031
1268
  /**
1032
- * @param {object} changes
1269
+ * @param {Record<string, any>} changes
1033
1270
  */
1034
1271
  constructor(changes = {}) {
1035
1272
  this._attributes = {}
1036
1273
  this._changes = {}
1037
1274
  this._isNewRecord = true
1038
- this._relationships = {}
1039
1275
 
1040
1276
  for (const key in changes) {
1041
1277
  this.setAttribute(key, changes[key])
@@ -1064,7 +1300,7 @@ class VelociousDatabaseRecord {
1064
1300
 
1065
1301
  /**
1066
1302
  * Returns a the current attributes of the record (original attributes from database plus changes)
1067
- * @returns {void}
1303
+ * @returns {Record<string, any>}
1068
1304
  */
1069
1305
  attributes() {
1070
1306
  return Object.assign({}, this._attributes, this._changes)
@@ -1076,20 +1312,22 @@ class VelociousDatabaseRecord {
1076
1312
  _connection() {
1077
1313
  if (this.__connection) return this.__connection
1078
1314
 
1079
- return this.constructor.connection()
1315
+ return this.getModelClass().connection()
1080
1316
  }
1081
1317
 
1082
1318
  /**
1083
1319
  * Destroys the record in the database and all of its dependent records.
1084
- * @returns {void}
1320
+ * @returns {Promise<void>}
1085
1321
  */
1086
1322
  async destroy() {
1087
- for (const relationship of this.constructor.getRelationships()) {
1323
+ for (const relationship of this.getModelClass().getRelationships()) {
1088
1324
  if (relationship.getDependent() != "destroy") {
1089
1325
  continue
1090
1326
  }
1091
1327
 
1092
1328
  const instanceRelationship = this.getRelationshipByName(relationship.getRelationshipName())
1329
+
1330
+ /** @type {VelociousDatabaseRecord[]} */
1093
1331
  let models
1094
1332
 
1095
1333
  if (instanceRelationship.getType() == "belongsTo") {
@@ -1099,13 +1337,23 @@ class VelociousDatabaseRecord {
1099
1337
 
1100
1338
  const model = instanceRelationship.loaded()
1101
1339
 
1102
- models = [model]
1340
+ if (model instanceof VelociousDatabaseRecord) {
1341
+ models = [model]
1342
+ } else {
1343
+ throw new Error(`Unexpected loaded type: ${typeof model}`)
1344
+ }
1103
1345
  } else if (instanceRelationship.getType() == "hasMany") {
1104
1346
  if (!instanceRelationship.isLoaded()) {
1105
1347
  await instanceRelationship.load()
1106
1348
  }
1107
1349
 
1108
- models = instanceRelationship.loaded()
1350
+ const loadedModels = instanceRelationship.loaded()
1351
+
1352
+ if (Array.isArray(loadedModels)) {
1353
+ models = loadedModels
1354
+ } else {
1355
+ throw new Error(`Unexpected loaded type: ${typeof loadedModels}`)
1356
+ }
1109
1357
  } else {
1110
1358
  throw new Error(`Unhandled relationship type: ${instanceRelationship.getType()}`)
1111
1359
  }
@@ -1117,9 +1365,10 @@ class VelociousDatabaseRecord {
1117
1365
  }
1118
1366
  }
1119
1367
 
1368
+ /** @type {Record<string, any>} */
1120
1369
  const conditions = {}
1121
1370
 
1122
- conditions[this.constructor.primaryKey()] = this.id()
1371
+ conditions[this.getModelClass().primaryKey()] = this.id()
1123
1372
 
1124
1373
  const sql = this._connection().deleteSql({
1125
1374
  conditions,
@@ -1129,9 +1378,7 @@ class VelociousDatabaseRecord {
1129
1378
  await this._connection().query(sql)
1130
1379
  }
1131
1380
 
1132
- /**
1133
- * @returns {boolean}
1134
- */
1381
+ /** @returns {boolean} */
1135
1382
  _hasChanges() { return Object.keys(this._changes).length > 0 }
1136
1383
 
1137
1384
  /**
@@ -1167,11 +1414,9 @@ class VelociousDatabaseRecord {
1167
1414
  return false
1168
1415
  }
1169
1416
 
1170
- /**
1171
- * Returns the changes that have been made to this record since it was loaded from the database.
1172
- * @returns {object}
1173
- */
1417
+ /** Returns the changes that have been made to this record since it was loaded from the database. */
1174
1418
  changes() {
1419
+ /** @type {Record<string, any[]>} */
1175
1420
  const changes = {}
1176
1421
 
1177
1422
  for (const changeKey in this._changes) {
@@ -1189,18 +1434,18 @@ class VelociousDatabaseRecord {
1189
1434
  _tableName() {
1190
1435
  if (this.__tableName) return this.__tableName
1191
1436
 
1192
- return this.constructor.tableName()
1437
+ return this.getModelClass().tableName()
1193
1438
  }
1194
1439
 
1195
1440
  /**
1196
1441
  * Reads an attribute value from the record.
1197
1442
  * @param {string} attributeName The name of the attribute to read. This is the attribute name, not the column name.
1198
- * @returns {void}
1443
+ * @returns {any}
1199
1444
  */
1200
1445
  readAttribute(attributeName) {
1201
- const columnName = this.constructor._attributeNameToColumnName[attributeName]
1446
+ const columnName = this.getModelClass().getAttributeNameToColumnNameMap()[attributeName]
1202
1447
 
1203
- if (!columnName) throw new Error(`Couldn't figure out column name for attribute: ${attributeName} from these mappings: ${Object.keys(this.constructor._attributeNameToColumnName)}`)
1448
+ if (!columnName) throw new Error(`Couldn't figure out column name for attribute: ${attributeName} from these mappings: ${Object.keys(this.getModelClass().getAttributeNameToColumnNameMap()).join(", ")}`)
1204
1449
 
1205
1450
  return this.readColumn(columnName)
1206
1451
  }
@@ -1210,7 +1455,7 @@ class VelociousDatabaseRecord {
1210
1455
  * @param {string} attributeName The name of the column to read. This is the column name, not the attribute name.
1211
1456
  */
1212
1457
  readColumn(attributeName) {
1213
- const column = this.constructor.getColumns().find((column) => column.getName() == attributeName)
1458
+ const column = this.getModelClass().getColumns().find((column) => column.getName() == attributeName)
1214
1459
  let result
1215
1460
 
1216
1461
  if (attributeName in this._changes) {
@@ -1221,7 +1466,7 @@ class VelociousDatabaseRecord {
1221
1466
  throw new Error(`No such attribute or not selected ${this.constructor.name}#${attributeName}`)
1222
1467
  }
1223
1468
 
1224
- if (column && this.constructor.getDatabaseType() == "sqlite") {
1469
+ if (column && this.getModelClass().getDatabaseType() == "sqlite") {
1225
1470
  if (result && (column.getType() == "date" || column.getType() == "datetime")) {
1226
1471
  result = new Date(Date.parse(result))
1227
1472
  }
@@ -1231,6 +1476,7 @@ class VelociousDatabaseRecord {
1231
1476
  }
1232
1477
 
1233
1478
  _belongsToChanges() {
1479
+ /** @type {Record<string, any>} */
1234
1480
  const belongsToChanges = {}
1235
1481
 
1236
1482
  if (this._instanceRelationships) {
@@ -1238,7 +1484,15 @@ class VelociousDatabaseRecord {
1238
1484
  const relationship = this._instanceRelationships[relationshipName]
1239
1485
 
1240
1486
  if (relationship.getType() == "belongsTo" && relationship.getDirty()) {
1241
- belongsToChanges[relationship.getForeignKey()] = relationship.loaded()?.id()
1487
+ const model = relationship.loaded()
1488
+
1489
+ if (model) {
1490
+ if (model instanceof VelociousDatabaseRecord) {
1491
+ belongsToChanges[relationship.getForeignKey()] = model?.id()
1492
+ } else {
1493
+ throw new Error(`Unexpected model type: ${typeof model}`)
1494
+ }
1495
+ }
1242
1496
  }
1243
1497
  }
1244
1498
  }
@@ -1247,29 +1501,29 @@ class VelociousDatabaseRecord {
1247
1501
  }
1248
1502
 
1249
1503
  /**
1250
- * @returns {void}
1504
+ * @returns {Promise<void>}
1251
1505
  */
1252
1506
  async _createNewRecord() {
1253
- if (!this.constructor.connection()["insertSql"]) {
1254
- throw new Error(`No insertSql on ${this.constructor.connection().constructor.name}`)
1507
+ if (!this.getModelClass().connection()["insertSql"]) {
1508
+ throw new Error(`No insertSql on ${this.getModelClass().connection().constructor.name}`)
1255
1509
  }
1256
1510
 
1257
- const createdAtColumn = this.constructor.getColumns().find((column) => column.getName() == "created_at")
1258
- const updatedAtColumn = this.constructor.getColumns().find((column) => column.getName() == "updated_at")
1511
+ const createdAtColumn = this.getModelClass().getColumns().find((column) => column.getName() == "created_at")
1512
+ const updatedAtColumn = this.getModelClass().getColumns().find((column) => column.getName() == "updated_at")
1259
1513
  const data = Object.assign({}, this._belongsToChanges(), this.attributes())
1260
1514
  const currentDate = new Date()
1261
1515
 
1262
1516
  if (createdAtColumn) data.created_at = currentDate
1263
1517
  if (updatedAtColumn) data.updated_at = currentDate
1264
1518
 
1265
- const columnNames = this.constructor.getColumnNames()
1519
+ const columnNames = this.getModelClass().getColumnNames()
1266
1520
  const sql = this._connection().insertSql({
1267
1521
  returnLastInsertedColumnNames: columnNames,
1268
1522
  tableName: this._tableName(),
1269
1523
  data
1270
1524
  })
1271
1525
  const insertResult = await this._connection().query(sql)
1272
- const primaryKey = this.constructor.primaryKey()
1526
+ const primaryKey = this.getModelClass().primaryKey()
1273
1527
 
1274
1528
  if (Array.isArray(insertResult) && insertResult[0] && insertResult[0][primaryKey]) {
1275
1529
  this._attributes = insertResult[0]
@@ -1283,10 +1537,10 @@ class VelociousDatabaseRecord {
1283
1537
  this.setIsNewRecord(false)
1284
1538
 
1285
1539
  // Mark all relationships as preloaded, since we don't expect anything to have magically appeared since we created the record.
1286
- for (const relationship of this.constructor.getRelationships()) {
1540
+ for (const relationship of this.getModelClass().getRelationships()) {
1287
1541
  const instanceRelationship = this.getRelationshipByName(relationship.getRelationshipName())
1288
1542
 
1289
- if (instanceRelationship.getType() == "hasMany" && instanceRelationship.getLoadedOrNull() === null) {
1543
+ if (instanceRelationship.getType() == "hasMany" && instanceRelationship.getLoadedOrUndefined() === null) {
1290
1544
  instanceRelationship.setLoaded([])
1291
1545
  }
1292
1546
 
@@ -1295,15 +1549,16 @@ class VelociousDatabaseRecord {
1295
1549
  }
1296
1550
 
1297
1551
  /**
1298
- * @returns {void}
1552
+ * @returns {Promise<void>}
1299
1553
  */
1300
1554
  async _updateRecordWithChanges() {
1555
+ /** @type {Record<string, any>} */
1301
1556
  const conditions = {}
1302
1557
 
1303
- conditions[this.constructor.primaryKey()] = this.id()
1558
+ conditions[this.getModelClass().primaryKey()] = this.id()
1304
1559
 
1305
1560
  const changes = Object.assign({}, this._belongsToChanges(), this._changes)
1306
- const updatedAtColumn = this.constructor.getColumns().find((column) => column.getName() == "updated_at")
1561
+ const updatedAtColumn = this.getModelClass().getColumns().find((column) => column.getName() == "updated_at")
1307
1562
  const currentDate = new Date()
1308
1563
 
1309
1564
  if (updatedAtColumn) changes.updated_at = currentDate
@@ -1323,15 +1578,15 @@ class VelociousDatabaseRecord {
1323
1578
  * @returns {number|string}
1324
1579
  */
1325
1580
  id() {
1326
- if (!this.constructor._columnNameToAttributeName) {
1581
+ if (!this.getModelClass()._columnNameToAttributeName) {
1327
1582
  throw new Error(`Column names mapping hasn't been set on ${this.constructor.name}. Has the model been initialized?`)
1328
1583
  }
1329
1584
 
1330
- const primaryKey = this.constructor.primaryKey()
1331
- const attributeName = this.constructor._columnNameToAttributeName[primaryKey]
1585
+ const primaryKey = this.getModelClass().primaryKey()
1586
+ const attributeName = this.getModelClass().getColumnNameToAttributeNameMap()[primaryKey]
1332
1587
 
1333
1588
  if (attributeName === undefined) {
1334
- throw new Error(`Primary key ${primaryKey} doesn't exist in columns: ${Object.keys(this.constructor._columnNameToAttributeName).join(", ")}`)
1589
+ throw new Error(`Primary key ${primaryKey} doesn't exist in columns: ${Object.keys(this.getModelClass().getColumnNameToAttributeNameMap()).join(", ")}`)
1335
1590
  }
1336
1591
 
1337
1592
  return this.readAttribute(attributeName)
@@ -1355,13 +1610,18 @@ class VelociousDatabaseRecord {
1355
1610
  this._isNewRecord = newIsNewRecord
1356
1611
  }
1357
1612
 
1613
+ /**
1614
+ * @param {string | number} id
1615
+ */
1358
1616
  async _reloadWithId(id) {
1359
- const primaryKey = this.constructor.primaryKey()
1617
+ const primaryKey = this.getModelClass().primaryKey()
1618
+
1619
+ /** @type {Record<string, any>} */
1360
1620
  const whereObject = {}
1361
1621
 
1362
1622
  whereObject[primaryKey] = id
1363
1623
 
1364
- const query = this.constructor.where(whereObject)
1624
+ const query = this.getModelClass().where(whereObject)
1365
1625
  const reloadedModel = await query.first()
1366
1626
 
1367
1627
  if (!reloadedModel) throw new Error(`${this.constructor.name}#${id} couldn't be reloaded - record didn't exist`)
@@ -1371,16 +1631,17 @@ class VelociousDatabaseRecord {
1371
1631
  }
1372
1632
 
1373
1633
  /**
1374
- * @returns {void}
1634
+ * @returns {Promise<void>}
1375
1635
  */
1376
1636
  async reload() {
1377
1637
  this._reloadWithId(this.readAttribute("id"))
1378
1638
  }
1379
1639
 
1380
1640
  async _runValidations() {
1641
+ /** @type {Record<string, {type: string, message: string}>} */
1381
1642
  this._validationErrors = {}
1382
1643
 
1383
- const validators = this.constructor._validators
1644
+ const validators = this.getModelClass()._validators
1384
1645
 
1385
1646
  if (validators) {
1386
1647
  for (const attributeName in validators) {
@@ -1403,15 +1664,16 @@ class VelociousDatabaseRecord {
1403
1664
  }
1404
1665
 
1405
1666
  /**
1406
- * @returns {Array<string>}
1667
+ * @returns {string[]}
1407
1668
  */
1408
1669
  fullErrorMessages() {
1670
+ /** @type {string[]} */
1409
1671
  const validationErrorMessages = []
1410
1672
 
1411
1673
  if (this._validationErrors) {
1412
1674
  for (const attributeName in this._validationErrors) {
1413
1675
  for (const validationError of this._validationErrors[attributeName]) {
1414
- const message = `${this.constructor.humanAttributeName(attributeName)} ${validationError.message}`
1676
+ const message = `${this.getModelClass().humanAttributeName(attributeName)} ${validationError.message}`
1415
1677
 
1416
1678
  validationErrorMessages.push(message)
1417
1679
  }
@@ -1432,6 +1694,16 @@ class VelociousDatabaseRecord {
1432
1694
  }
1433
1695
  }
1434
1696
 
1697
+ class TranslationBase extends VelociousDatabaseRecord {
1698
+ /**
1699
+ * @abstract
1700
+ * @returns {string}
1701
+ */
1702
+ locale() {
1703
+ throw new Error("'locale' not implemented")
1704
+ }
1705
+ }
1706
+
1435
1707
  VelociousDatabaseRecord.registerValidatorType("presence", ValidatorsPresence)
1436
1708
  VelociousDatabaseRecord.registerValidatorType("uniqueness", ValidatorsUniqueness)
1437
1709