velocious 1.0.5 → 1.0.6

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 (69) hide show
  1. package/package.json +5 -3
  2. package/spec/cli/commands/db/create-spec.mjs +1 -1
  3. package/spec/cli/commands/db/migrate-spec.mjs +2 -0
  4. package/spec/database/record/create-spec.mjs +9 -0
  5. package/spec/database/record/find-spec.mjs +0 -1
  6. package/spec/database/record/query-spec.mjs +37 -0
  7. package/spec/dummy/index.mjs +14 -2
  8. package/spec/dummy/src/config/configuration.example.mjs +16 -1
  9. package/spec/dummy/src/config/configuration.peakflow.mjs +16 -1
  10. package/spec/dummy/src/database/migrations/20230728075328-create-projects.mjs +0 -1
  11. package/spec/dummy/src/database/migrations/20230728075329-create-tasks.mjs +0 -1
  12. package/spec/dummy/src/database/migrations/20250605133926-create-project-translations.mjs +16 -0
  13. package/spec/dummy/src/models/project.mjs +9 -0
  14. package/spec/dummy/src/models/task.mjs +5 -1
  15. package/src/big-brother.mjs +37 -0
  16. package/src/cli/commands/db/create.mjs +8 -6
  17. package/src/cli/commands/db/migrate.mjs +1 -2
  18. package/src/cli/commands/generate/migration.mjs +1 -1
  19. package/src/cli/commands/generate/model.mjs +1 -1
  20. package/src/configuration.mjs +39 -3
  21. package/src/database/drivers/base.mjs +21 -2
  22. package/src/database/drivers/mysql/column.mjs +8 -0
  23. package/src/database/drivers/mysql/index.mjs +34 -2
  24. package/src/database/drivers/mysql/options.mjs +1 -0
  25. package/src/database/drivers/mysql/query-parser.mjs +2 -23
  26. package/src/database/drivers/mysql/table.mjs +25 -0
  27. package/src/database/drivers/sqlite/base.mjs +76 -4
  28. package/src/database/drivers/sqlite/column.mjs +10 -0
  29. package/src/database/drivers/sqlite/index.native.mjs +19 -22
  30. package/src/database/drivers/sqlite/index.web.mjs +27 -17
  31. package/src/database/drivers/sqlite/options.mjs +2 -1
  32. package/src/database/drivers/sqlite/query-parser.mjs +2 -23
  33. package/src/database/drivers/sqlite/query.native.mjs +16 -1
  34. package/src/database/drivers/sqlite/query.web.mjs +27 -2
  35. package/src/database/drivers/sqlite/sql/create-index.mjs +4 -0
  36. package/src/database/drivers/sqlite/table.mjs +16 -1
  37. package/src/database/initializer-from-require-context.mjs +21 -0
  38. package/src/database/migration/index.mjs +34 -2
  39. package/src/database/migrator.mjs +4 -2
  40. package/src/database/pool/base.mjs +5 -0
  41. package/src/database/query/base.mjs +2 -1
  42. package/src/database/query/create-index-base.mjs +50 -0
  43. package/src/database/query/create-table-base.mjs +40 -17
  44. package/src/database/query/index.mjs +83 -21
  45. package/src/database/query/preloader/belongs-to.mjs +52 -0
  46. package/src/database/query/preloader/has-many.mjs +55 -0
  47. package/src/database/query/preloader.mjs +41 -0
  48. package/src/database/query/where-base.mjs +9 -0
  49. package/src/database/query/where-hash.mjs +35 -0
  50. package/src/database/query/where-plain.mjs +13 -0
  51. package/src/database/query-parser/base-query-parser.mjs +33 -0
  52. package/src/database/query-parser/group-parser.mjs +40 -0
  53. package/src/database/query-parser/joins-parser.mjs +48 -7
  54. package/src/database/query-parser/limit-parser.mjs +40 -0
  55. package/src/database/query-parser/options.mjs +2 -1
  56. package/src/database/query-parser/order-parser.mjs +39 -0
  57. package/src/database/query-parser/select-parser.mjs +5 -1
  58. package/src/database/query-parser/where-parser.mjs +39 -0
  59. package/src/database/record/index.mjs +464 -29
  60. package/src/database/record/instance-relationships/base.mjs +28 -0
  61. package/src/database/record/instance-relationships/belongs-to.mjs +20 -0
  62. package/src/database/record/instance-relationships/has-many.mjs +47 -0
  63. package/src/database/record/relationships/base.mjs +32 -0
  64. package/src/database/record/relationships/belongs-to.mjs +12 -0
  65. package/src/database/record/relationships/has-many.mjs +12 -0
  66. package/src/database/table-data/index.mjs +15 -25
  67. package/src/http-server/worker-handler/worker-thread.mjs +7 -4
  68. package/src/templates/generate-model.mjs +3 -1
  69. package/src/utils/rest-args-error.mjs +9 -0
@@ -1,24 +1,130 @@
1
+ import BelongsToInstanceRelationship from "./instance-relationships/belongs-to.mjs"
2
+ import BelongsToRelationship from "./relationships/belongs-to.mjs"
1
3
  import Configuration from "../../configuration.mjs"
4
+ import FromTable from "../query/from-table.mjs"
2
5
  import Handler from "../handler.mjs"
3
- import inflection from "inflection"
6
+ import HasManyRelationship from "./relationships/has-many.mjs"
7
+ import HasManyInstanceRelationship from "./instance-relationships/has-many.mjs"
8
+ import * as inflection from "inflection"
4
9
  import Query from "../query/index.mjs"
5
10
  import RecordNotFoundError from "./record-not-found-error.mjs"
6
11
 
7
12
  export default class VelociousDatabaseRecord {
13
+ static _relationshipExists(relationshipName) {
14
+ if (this._relationships && relationshipName in this._relationships) {
15
+ return true
16
+ }
17
+
18
+ return false
19
+ }
20
+
21
+ static _defineRelationship(relationshipName, data) {
22
+ if (!this._relationships) this._relationships = {}
23
+ if (this._relationshipExists(relationshipName)) throw new Error(`Relationship ${relationshipName} already exists`)
24
+
25
+ const actualData = Object.assign(
26
+ {
27
+ modelClass: this,
28
+ relationshipName,
29
+ type: "hasMany"
30
+ },
31
+ data
32
+ )
33
+
34
+ if (!actualData.className && !actualData.klass) {
35
+ actualData.className = inflection.camelize(inflection.singularize(relationshipName))
36
+ }
37
+
38
+ let relationship
39
+
40
+ if (actualData.type == "belongsTo") {
41
+ relationship = new BelongsToRelationship(actualData)
42
+
43
+ const buildMethodName = `build${inflection.camelize(relationshipName)}`
44
+
45
+ this.prototype[relationshipName] = function () {
46
+ const relationship = this.getRelationshipByName(relationshipName)
47
+
48
+ return relationship.loaded()
49
+ }
50
+
51
+ this.prototype[buildMethodName] = function (attributes) {
52
+ const relationship = this.getRelationshipByName(relationshipName)
53
+ const record = relationship.build(attributes)
54
+
55
+ return record
56
+ }
57
+ } else if (actualData.type == "hasMany") {
58
+ relationship = new HasManyRelationship(actualData)
59
+
60
+ this.prototype[relationshipName] = function () {
61
+ return this.getRelationshipByName(relationshipName)
62
+ }
63
+ } else {
64
+ throw new Error(`Unknown relationship type: ${actualData.type}`)
65
+ }
66
+
67
+ this._relationships[relationshipName] = relationship
68
+ }
69
+
70
+ static getRelationshipByName(relationshipName) {
71
+ if (!this._relationships) this._relationships = {}
72
+
73
+ const relationship = this._relationships[relationshipName]
74
+
75
+ if (!relationship) throw new Error(`No relationship by that name: ${relationshipName}`)
76
+
77
+ return relationship
78
+ }
79
+
80
+ getRelationshipByName(relationshipName) {
81
+ if (!this._instanceRelationships) this._instanceRelationships = {}
82
+
83
+ if (!(relationshipName in this._instanceRelationships)) {
84
+ const modelClassRelationship = this.constructor.getRelationshipByName(relationshipName)
85
+ let instanceRelationship
86
+
87
+ if (modelClassRelationship.getType() == "belongsTo") {
88
+ instanceRelationship = new BelongsToInstanceRelationship({model: this, relationship: modelClassRelationship})
89
+ } else if (modelClassRelationship.getType() == "hasMany") {
90
+ instanceRelationship = new HasManyInstanceRelationship({model: this, relationship: modelClassRelationship})
91
+ } else {
92
+ throw new Error(`Unknown relationship type: ${modelClassRelationship.getType()}`)
93
+ }
94
+
95
+ this._instanceRelationships[relationshipName] = instanceRelationship
96
+ }
97
+
98
+ return this._instanceRelationships[relationshipName]
99
+ }
100
+
101
+ static belongsTo(relationshipName) {
102
+ this._defineRelationship(relationshipName, {type: "belongsTo"})
103
+ }
104
+
8
105
  static connection() {
9
- const connection = Configuration.current().getDatabasePoolType().current().getCurrentConnection()
106
+ const connection = this._getConfiguration().getDatabasePoolType().current().getCurrentConnection()
10
107
 
11
108
  if (!connection) throw new Error("No connection?")
12
109
 
13
110
  return connection
14
111
  }
15
112
 
113
+ static async create(attributes) {
114
+ const record = new this(attributes)
115
+
116
+ await record.save()
117
+
118
+ return record
119
+ }
120
+
16
121
  static async find(recordId) {
17
122
  const conditions = {}
18
123
 
19
124
  conditions[this.primaryKey()] = recordId
20
125
 
21
- const record = await this.where(conditions).first()
126
+ const query = this.where(conditions)
127
+ const record = await query.first()
22
128
 
23
129
  if (!record) {
24
130
  throw new RecordNotFoundError(`Couldn't find ${this.name} with '${this.primaryKey()}'=${recordId}`)
@@ -27,8 +133,144 @@ export default class VelociousDatabaseRecord {
27
133
  return record
28
134
  }
29
135
 
136
+ static _getConfiguration() {
137
+ if (!this._configuration) {
138
+ this._configuration = Configuration.current()
139
+
140
+ if (!this._configuration) {
141
+ throw new Error("Configuration hasn't been set (model class probably hasn't been initialized)")
142
+ }
143
+ }
144
+
145
+ return this._configuration
146
+ }
147
+
148
+ _getConfiguration() {
149
+ return this.constructor._getConfiguration()
150
+ }
151
+
152
+ static hasMany(relationshipName, options = {}) {
153
+ return this._defineRelationship(relationshipName, Object.assign({type: "hasMany"}, options))
154
+ }
155
+
156
+ static async initializeRecord({configuration}) {
157
+ if (!configuration) throw new Error("No configuration given")
158
+
159
+ this._configuration = configuration
160
+ this._configuration.registerModelClass(this)
161
+
162
+ this._table = await this.connection().getTableByName(this.tableName())
163
+ this._columns = await this._getTable().getColumns()
164
+ this._columnsAsHash = {}
165
+
166
+ for (const column of this._columns) {
167
+ this._columnsAsHash[column.getName()] = column
168
+
169
+ const camelizedColumnName = inflection.camelize(column.getName(), true)
170
+ const camelizedColumnNameBigFirst = inflection.camelize(column.getName())
171
+ const setterMethodName = `set${camelizedColumnNameBigFirst}`
172
+
173
+ this.prototype[camelizedColumnName] = function () {
174
+ return this.readAttribute(camelizedColumnName)
175
+ }
176
+
177
+ this.prototype[setterMethodName] = function (newValue) {
178
+ return this._setColumnAttribute(camelizedColumnName, newValue)
179
+ }
180
+ }
181
+
182
+ await this._defineTranslationMethods()
183
+ this._initialized = true
184
+ }
185
+
186
+ static isInitialized() {
187
+ if (this._initialized) return true
188
+
189
+ return false
190
+ }
191
+
192
+ static async _defineTranslationMethods() {
193
+ if (this._translations) {
194
+ const locales = this._getConfiguration().getLocales()
195
+
196
+ if (!locales) throw new Error("Locales hasn't been set in the configuration")
197
+
198
+ await this.getTranslationClass().initializeRecord({configuration: this._getConfiguration()})
199
+
200
+ for (const name in this._translations) {
201
+ const nameCamelized = inflection.camelize(name)
202
+ const setterMethodName = `set${nameCamelized}`
203
+
204
+ this.prototype[name] = function () {
205
+ const locale = this._getConfiguration().getLocale()
206
+
207
+ return this._getTranslatedAttribute(name, locale)
208
+ }
209
+
210
+ this.prototype[setterMethodName] = function (newValue) {
211
+ const locale = this._getConfiguration().getLocale()
212
+
213
+ return this._setTranslatedAttribute(name, locale, newValue)
214
+ }
215
+
216
+ for (const locale of locales) {
217
+ const localeCamelized = inflection.camelize(locale)
218
+ const getterMethodNameLocalized = `${name}${localeCamelized}`
219
+ const setterMethodNameLocalized = `${setterMethodName}${localeCamelized}`
220
+
221
+ this.prototype[getterMethodNameLocalized] = function () {
222
+ return this._getTranslatedAttribute(name, locale)
223
+ }
224
+
225
+ this.prototype[setterMethodNameLocalized] = function (newValue) {
226
+ return this._setTranslatedAttribute(name, locale, newValue)
227
+ }
228
+ }
229
+ }
230
+ }
231
+ }
232
+
233
+ getAttribute(name) {
234
+ const columnName = inflection.underscore(name)
235
+
236
+ if (!this.isNewRecord() && !(columnName in this._attributes)) {
237
+ throw new Error(`${this.constructor.name}#${name} attribute hasn't been loaded yet in ${Object.keys(this._attributes).join(", ")}`)
238
+ }
239
+
240
+ return this._attributes[columnName]
241
+ }
242
+
243
+ setAttribute(name, newValue) {
244
+ const setterName = `set${inflection.camelize(name)}`
245
+
246
+ if (!this.constructor.isInitialized()) throw new Error(`${this.constructor.name} model isn't initialized yet`)
247
+ if (!(setterName in this)) throw new Error(`No such setter method: ${this.constructor.name}#${setterName}`)
248
+
249
+ this[setterName](newValue)
250
+ }
251
+
252
+ _setColumnAttribute(name, newValue) {
253
+ const columnName = inflection.underscore(name)
254
+
255
+ if (this._attributes[columnName] != newValue) {
256
+ this._changes[columnName] = newValue
257
+ }
258
+ }
259
+
260
+ static getColumns() {
261
+ if (!this._columns) throw new Error(`${this.name} hasn't been initialized yet`)
262
+
263
+ return this._columns
264
+ }
265
+
266
+ static _getTable() {
267
+ if (!this._table) throw new Error(`${this.name} hasn't been initialized yet`)
268
+
269
+ return this._table
270
+ }
271
+
30
272
  static async last() {
31
- const query = this._newQuery().order(this.primaryKey()).limit(1)
273
+ const query = this._newQuery().order(this.primaryKey())
32
274
  const record = await query.last()
33
275
 
34
276
  return record
@@ -39,10 +281,69 @@ export default class VelociousDatabaseRecord {
39
281
  }
40
282
 
41
283
  async save() {
284
+ let result
285
+
286
+ const isNewRecord = this.isNewRecord()
287
+
288
+ await this._autoSaveBelongsToRelationships()
289
+
42
290
  if (this.isPersisted()) {
43
- return await this._updateRecordWithChanges()
291
+ result = await this._updateRecordWithChanges()
44
292
  } else {
45
- return await this._createNewRecord()
293
+ result = await this._createNewRecord()
294
+ }
295
+
296
+ await this._autoSaveHasManyRelationships({isNewRecord})
297
+
298
+ return result
299
+ }
300
+
301
+ async _autoSaveBelongsToRelationships() {
302
+ for (const relationshipName in this._instanceRelationships) {
303
+ const instanceRelationship = this._instanceRelationships[relationshipName]
304
+
305
+ if (instanceRelationship.getType() != "belongsTo") {
306
+ continue
307
+ }
308
+
309
+ const model = instanceRelationship.loaded()
310
+
311
+ if (model.isChanged()) {
312
+ await model.save()
313
+
314
+ const foreignKey = instanceRelationship.getForeignKey()
315
+
316
+ this.setAttribute(foreignKey, model.id())
317
+ instanceRelationship.setPreloaded(true)
318
+ }
319
+ }
320
+ }
321
+
322
+ async _autoSaveHasManyRelationships({isNewRecord}) {
323
+ for (const relationshipName in this._instanceRelationships) {
324
+ const instanceRelationship = this._instanceRelationships[relationshipName]
325
+
326
+ if (instanceRelationship.getType() != "hasMany") {
327
+ continue
328
+ }
329
+
330
+ let loaded = instanceRelationship._loaded
331
+
332
+ if (!Array.isArray(loaded)) loaded = [loaded]
333
+
334
+ for (const model of loaded) {
335
+ const foreignKey = instanceRelationship.getForeignKey()
336
+
337
+ model.setAttribute(foreignKey, this.id())
338
+
339
+ if (model.isChanged()) {
340
+ await model.save()
341
+ }
342
+ }
343
+
344
+ if (isNewRecord) {
345
+ instanceRelationship.setPreloaded(true)
346
+ }
46
347
  }
47
348
  }
48
349
 
@@ -50,6 +351,75 @@ export default class VelociousDatabaseRecord {
50
351
  return inflection.underscore(inflection.pluralize(this.name))
51
352
  }
52
353
 
354
+ static setTableName(tableName) {
355
+ this._tableName = tableName
356
+ }
357
+
358
+ static translates(...names) {
359
+ for (const name of names) {
360
+ if (!this._translations) this._translations = {}
361
+ if (name in this._translations) throw new Error(`Translation already exists: ${name}`)
362
+
363
+ this._translations[name] = {}
364
+
365
+ if (!this._relationshipExists("translations")) {
366
+ this._defineRelationship("translations", {klass: this.getTranslationClass(), type: "hasMany"})
367
+ }
368
+ }
369
+ }
370
+
371
+ static getTranslationClass() {
372
+ if (this._translationClass) return this._translationClass
373
+ if (this.tableName().endsWith("_translations")) throw new Error("Trying to define a translations class for a translation class")
374
+
375
+ const className = `${this.name}Translation`
376
+ const TranslationClass = class Translation extends VelociousDatabaseRecord {}
377
+ const belongsTo = `${inflection.camelize(inflection.singularize(this.tableName()), true)}`
378
+ const tableName = `${inflection.singularize(this.tableName())}_translations`
379
+
380
+ Object.defineProperty(TranslationClass, "name", {value: className})
381
+ TranslationClass.setTableName(tableName)
382
+ TranslationClass.belongsTo(belongsTo)
383
+
384
+ this._translationClass = TranslationClass
385
+
386
+ return this._translationClass
387
+ }
388
+
389
+ static getTranslationsTableName() {
390
+ return `${inflection.singularize(this.tableName())}_translations`
391
+ }
392
+
393
+ static async hasTranslationsTable() {
394
+ try {
395
+ await this.connection().getTableByName(this.getTranslationsTableName())
396
+
397
+ return true
398
+ } catch {
399
+ return false
400
+ }
401
+ }
402
+
403
+ _getTranslatedAttribute(name, locale) {
404
+ const translation = this.translations().loaded().find((translation) => translation.locale() == locale)
405
+
406
+ if (translation) {
407
+ return translation[name]()
408
+ }
409
+ }
410
+
411
+ _setTranslatedAttribute(name, locale, newValue) {
412
+ let translation = this.translations().loaded()?.find((translation) =>translation.locale() == locale)
413
+
414
+ if (!translation) translation = this.translations().build({locale})
415
+
416
+ const assignments = {}
417
+
418
+ assignments[name] = newValue
419
+
420
+ translation.assign(assignments)
421
+ }
422
+
53
423
  static _newQuery() {
54
424
  const handler = new Handler()
55
425
  const query = new Query({
@@ -58,29 +428,62 @@ export default class VelociousDatabaseRecord {
58
428
  modelClass: this
59
429
  })
60
430
 
61
- return query.from(this.tableName())
431
+ return query.from(new FromTable({driver: this.connection(), tableName: this.tableName()}))
62
432
  }
63
433
 
64
434
  static orderableColumn() {
65
- // Allow to change to 'created_at' if using UUID?
435
+ // FIXME: Allow to change to 'created_at' if using UUID?
66
436
 
67
437
  return this.primaryKey()
68
438
  }
69
439
 
70
- static where(object) {
71
- const query = this._newQuery().where(object)
440
+ static all() {
441
+ return this._newQuery()
442
+ }
72
443
 
73
- return query
444
+ static async findBy(...args) {
445
+ return this._newQuery().findBy(...args)
74
446
  }
75
447
 
76
- constructor(attributes = {}) {
77
- this._attributes = attributes
448
+ static async findOrCreateBy(...args) {
449
+ return this._newQuery().findOrCreateBy(...args)
450
+ }
451
+
452
+ static async findOrInitializeBy(...args) {
453
+ return this._newQuery().findOrInitializeBy(...args)
454
+ }
455
+
456
+ static joins(...args) {
457
+ return this._newQuery().joins(...args)
458
+ }
459
+
460
+ static preload(...args) {
461
+ return this._newQuery().preload(...args)
462
+ }
463
+
464
+ static where(...args) {
465
+ return this._newQuery().where(...args)
466
+ }
467
+
468
+ constructor(changes = {}) {
469
+ this._attributes = {}
78
470
  this._changes = {}
471
+ this._isNewRecord = true
472
+ this._relationships = {}
473
+
474
+ for (const key in changes) {
475
+ this.setAttribute(key, changes[key])
476
+ }
477
+ }
478
+
479
+ loadExistingRecord(attributes) {
480
+ this._attributes = attributes
481
+ this._isNewRecord = false
79
482
  }
80
483
 
81
484
  assign(attributesToAssign) {
82
485
  for (const attributeToAssign in attributesToAssign) {
83
- this._changes[attributeToAssign] = attributesToAssign[attributeToAssign]
486
+ this.setAttribute(attributeToAssign, attributesToAssign[attributeToAssign])
84
487
  }
85
488
  }
86
489
 
@@ -107,6 +510,33 @@ export default class VelociousDatabaseRecord {
107
510
  await this._connection().query(sql)
108
511
  }
109
512
 
513
+ _hasChanges = () => Object.keys(this._changes).length > 0
514
+
515
+ isChanged() {
516
+ if (this.isNewRecord() || this._hasChanges()) {
517
+ return true
518
+ }
519
+
520
+ // Check if a loaded sub-model of a relationship is changed and should be saved along with this model.
521
+ if (this._instanceRelationships) {
522
+ for (const instanceRelationshipName in this._instanceRelationships) {
523
+ const instanceRelationship = this._instanceRelationships[instanceRelationshipName]
524
+ let loaded = instanceRelationship._loaded
525
+
526
+ if (!loaded) continue
527
+ if (!Array.isArray(loaded)) loaded = [loaded]
528
+
529
+ for (const model of loaded) {
530
+ if (model.isChanged()) {
531
+ return true
532
+ }
533
+ }
534
+ }
535
+ }
536
+
537
+ return false
538
+ }
539
+
110
540
  _tableName() {
111
541
  if (this.__tableName) return this.__tableName
112
542
 
@@ -114,9 +544,15 @@ export default class VelociousDatabaseRecord {
114
544
  }
115
545
 
116
546
  readAttribute(attributeName) {
117
- if (attributeName in this._changes) return this._changes[attributeName]
547
+ const attributeNameUnderscore = inflection.underscore(attributeName)
118
548
 
119
- return this._attributes[attributeName]
549
+ if (attributeNameUnderscore in this._changes) return this._changes[attributeNameUnderscore]
550
+
551
+ if (!(attributeNameUnderscore in this._attributes) && this.isPersisted()) {
552
+ throw new Error(`No such attribute or not selected ${this.constructor.name}#${attributeName}`)
553
+ }
554
+
555
+ return this._attributes[attributeNameUnderscore]
120
556
  }
121
557
 
122
558
  async _createNewRecord() {
@@ -128,13 +564,16 @@ export default class VelociousDatabaseRecord {
128
564
  tableName: this._tableName(),
129
565
  data: this.attributes()
130
566
  })
131
- const result = await this._connection().query(sql)
132
- const id = result.insertId
567
+ await this._connection().query(sql)
568
+ const id = await this._connection().lastInsertID()
133
569
 
134
570
  await this._reloadWithId(id)
571
+ this.setIsNewRecord(false)
135
572
  }
136
573
 
137
574
  async _updateRecordWithChanges() {
575
+ if (!this._hasChanges()) return
576
+
138
577
  const conditions = {}
139
578
 
140
579
  conditions[this.constructor.primaryKey()] = this.id()
@@ -148,18 +587,12 @@ export default class VelociousDatabaseRecord {
148
587
  await this._reloadWithId(this.id())
149
588
  }
150
589
 
151
- id() {
152
- return this.readAttribute(this.constructor.primaryKey())
153
- }
590
+ id = () => this.readAttribute(this.constructor.primaryKey())
591
+ isPersisted = () => !this._isNewRecord
592
+ isNewRecord = () => this._isNewRecord
154
593
 
155
- isPersisted() {
156
- if (this.id()) return true
157
-
158
- return false
159
- }
160
-
161
- isNewRecord() {
162
- return !this.isPersisted()
594
+ setIsNewRecord(newIsNewRecord) {
595
+ this._isNewRecord = newIsNewRecord
163
596
  }
164
597
 
165
598
  async _reloadWithId(id) {
@@ -171,6 +604,8 @@ export default class VelociousDatabaseRecord {
171
604
  const query = this.constructor.where(whereObject)
172
605
  const reloadedModel = await query.first()
173
606
 
607
+ if (!reloadedModel) throw new Error(`${this.constructor.name}#${this.id()} couldn't be reloaded - record didn't exist`)
608
+
174
609
  this._attributes = reloadedModel.attributes()
175
610
  this._changes = {}
176
611
  }
@@ -0,0 +1,28 @@
1
+ export default class VelociousDatabaseRecordBaseInstanceRelationship {
2
+ constructor({model, relationship}) {
3
+ this.model = model
4
+ this.relationship = relationship
5
+ }
6
+
7
+ loaded() {
8
+ if (!this._preloaded && this.model.isPersisted()) {
9
+ throw new Error(`${this.model.constructor.name}#${this.relationship.getRelationshipName()} hasn't been preloaded`)
10
+ }
11
+
12
+ return this._loaded
13
+ }
14
+
15
+ setLoaded(model) {
16
+ this._loaded = model
17
+ }
18
+
19
+ setPreloaded(preloadedValue) {
20
+ this._preloaded = preloadedValue
21
+ }
22
+
23
+ getForeignKey = () => this.getRelationship().getForeignKey()
24
+ getPrimaryKey = () => this.getRelationship().getPrimaryKey()
25
+ getRelationship = () => this.relationship
26
+ getTargetModelClass = () => this.getRelationship().getTargetModelClass()
27
+ getType = () => this.getRelationship().getType()
28
+ }
@@ -0,0 +1,20 @@
1
+ import BaseInstanceRelationship from "./base.mjs"
2
+
3
+ export default class VelociousDatabaseRecordBelongsToInstanceRelationship extends BaseInstanceRelationship {
4
+ constructor(args) {
5
+ super(args)
6
+ }
7
+
8
+ build(data) {
9
+ const targetModelClass = this.getTargetModelClass()
10
+ const newInstance = new targetModelClass(data)
11
+
12
+ this._loaded = newInstance
13
+
14
+ return newInstance
15
+ }
16
+
17
+ setLoaded(models) {
18
+ this._loaded = models
19
+ }
20
+ }
@@ -0,0 +1,47 @@
1
+ import BaseInstanceRelationship from "./base.mjs"
2
+
3
+ export default class VelociousDatabaseRecordHasManyInstanceRelationship extends BaseInstanceRelationship {
4
+ constructor(args) {
5
+ super(args)
6
+ this._loaded = []
7
+ }
8
+
9
+ build(data) {
10
+ const targetModelClass = this.getTargetModelClass()
11
+ const newInstance = new targetModelClass(data)
12
+
13
+ this._loaded.push(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
+ addToLoaded(models) {
27
+ if (Array.isArray(models)) {
28
+ for (const model of models) {
29
+ this._loaded.push(model)
30
+ }
31
+ } else {
32
+ this._loaded.push(models)
33
+ }
34
+ }
35
+
36
+ setLoaded(models) {
37
+ if (!Array.isArray(models)) throw new Error(`Argument given to setLoaded wasn't an array: ${typeof models}`)
38
+
39
+ this._loaded = models
40
+ }
41
+
42
+ setPreloaded(preloadedValue) {
43
+ this._preloaded = preloadedValue
44
+ }
45
+
46
+ getTargetModelClass = () => this.relationship.getTargetModelClass()
47
+ }