velocious 1.0.35 → 1.0.37

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 (44) hide show
  1. package/README.md +1 -0
  2. package/package.json +5 -3
  3. package/peak_flow.yml +22 -2
  4. package/spec/cli/commands/db/create-spec.js +6 -0
  5. package/spec/database/drivers/mysql/connection-spec.js +1 -1
  6. package/spec/database/record/create-spec.js +24 -0
  7. package/spec/database/record/update-spec.js +15 -0
  8. package/spec/database/record/validations-spec.js +28 -0
  9. package/spec/database/transactions-spec.js +36 -0
  10. package/spec/dummy/src/config/{configuration.sqlite.js → configuration.peakflow.pgsql.js} +11 -38
  11. package/spec/dummy/src/models/task.js +1 -0
  12. package/src/cli/commands/db/create.js +15 -10
  13. package/src/cli/commands/test.js +8 -0
  14. package/src/configuration.js +14 -0
  15. package/src/database/drivers/base.js +21 -4
  16. package/src/database/drivers/mssql/index.js +12 -3
  17. package/src/database/drivers/mssql/sql/create-database.js +1 -1
  18. package/src/database/drivers/mysql/index.js +2 -2
  19. package/src/database/drivers/pgsql/column.js +10 -0
  20. package/src/database/drivers/pgsql/foreign-key.js +13 -0
  21. package/src/database/drivers/pgsql/index.js +172 -0
  22. package/src/database/drivers/pgsql/options.js +18 -0
  23. package/src/database/drivers/pgsql/query-parser.js +4 -0
  24. package/src/database/drivers/pgsql/sql/create-database.js +37 -0
  25. package/src/database/drivers/pgsql/sql/create-index.js +4 -0
  26. package/src/database/drivers/pgsql/sql/create-table.js +4 -0
  27. package/src/database/drivers/pgsql/sql/delete.js +19 -0
  28. package/src/database/drivers/pgsql/sql/drop-table.js +4 -0
  29. package/src/database/drivers/pgsql/sql/insert.js +4 -0
  30. package/src/database/drivers/pgsql/sql/update.js +31 -0
  31. package/src/database/drivers/pgsql/table.js +62 -0
  32. package/src/database/drivers/sqlite/base.js +4 -0
  33. package/src/database/drivers/sqlite/column.js +10 -0
  34. package/src/database/migrator.js +1 -1
  35. package/src/database/query/create-database-base.js +1 -1
  36. package/src/database/query/create-table-base.js +16 -2
  37. package/src/database/query/index.js +10 -1
  38. package/src/database/query/insert-base.js +1 -1
  39. package/src/database/record/index.js +162 -18
  40. package/src/database/record/validators/base.js +2 -0
  41. package/src/database/record/validators/presence.js +13 -0
  42. package/src/database/record/validators/uniqueness.js +23 -0
  43. package/src/testing/test-runner.js +10 -0
  44. package/spec/dummy/src/config/configuration.mariadb.js +0 -97
@@ -7,8 +7,26 @@ import HasManyRelationship from "./relationships/has-many.js"
7
7
  import HasManyInstanceRelationship from "./instance-relationships/has-many.js"
8
8
  import * as inflection from "inflection"
9
9
  import Query from "../query/index.js"
10
+ import ValidatorsPresence from "./validators/presence.js"
11
+ import ValidatorsUniqueness from "./validators/uniqueness.js"
12
+
13
+ class VelociousDatabaseRecord {
14
+ static validatorTypes() {
15
+ if (!this._validatorTypes) this._validatorTypes = {}
16
+
17
+ return this._validatorTypes
18
+ }
19
+
20
+ static registerValidatorType(name, validatorClass) {
21
+ this.validatorTypes()[name] = validatorClass
22
+ }
23
+
24
+ static getValidatorType(validatorName) {
25
+ if (!(validatorName in this.validatorTypes())) throw new Error(`Validator type ${validatorName} not found`)
26
+
27
+ return this.validatorTypes()[validatorName]
28
+ }
10
29
 
11
- export default class VelociousDatabaseRecord {
12
30
  static _relationshipExists(relationshipName) {
13
31
  if (this._relationships && relationshipName in this._relationships) {
14
32
  return true
@@ -153,6 +171,12 @@ export default class VelociousDatabaseRecord {
153
171
  return this._defineRelationship(relationshipName, Object.assign({type: "hasMany"}, options))
154
172
  }
155
173
 
174
+ static humanAttributeName(attributeName) {
175
+ const modelNameKey = inflection.underscore(this.constructor.name)
176
+
177
+ return this._getConfiguration().getTranslator()(`velocious.database.record.attributes.${modelNameKey}.${attributeName}`, {defaultValue: inflection.camelize(attributeName)})
178
+ }
179
+
156
180
  static async initializeRecord({configuration}) {
157
181
  if (!configuration) throw new Error(`No configuration given for ${this.name}`)
158
182
 
@@ -296,11 +320,19 @@ export default class VelociousDatabaseRecord {
296
320
  const isNewRecord = this.isNewRecord()
297
321
  let result
298
322
 
323
+ await this._runValidations()
324
+
299
325
  await this.constructor.transaction(async () => {
300
- await this._autoSaveBelongsToRelationships()
326
+ // If any belongs-to-relationships was saved, then updated-at should still be set on this record.
327
+ const {savedCount} = await this._autoSaveBelongsToRelationships()
301
328
 
302
329
  if (this.isPersisted()) {
303
- result = await this._updateRecordWithChanges()
330
+ // If any has-many-relationships will be saved, then updated-at should still be set on this record.
331
+ const autoSaveHasManyrelationships = this._autoSaveHasManyRelationshipsToSave()
332
+
333
+ if (this._hasChanges() || savedCount > 0 || autoSaveHasManyrelationships.length > 0) {
334
+ result = await this._updateRecordWithChanges()
335
+ }
304
336
  } else {
305
337
  result = await this._createNewRecord()
306
338
  }
@@ -312,6 +344,8 @@ export default class VelociousDatabaseRecord {
312
344
  }
313
345
 
314
346
  async _autoSaveBelongsToRelationships() {
347
+ let savedCount = 0
348
+
315
349
  for (const relationshipName in this._instanceRelationships) {
316
350
  const instanceRelationship = this._instanceRelationships[relationshipName]
317
351
 
@@ -329,11 +363,17 @@ export default class VelociousDatabaseRecord {
329
363
  this.setAttribute(foreignKey, model.id())
330
364
  instanceRelationship.setPreloaded(true)
331
365
  instanceRelationship.setDirty(false)
366
+
367
+ savedCount++
332
368
  }
333
369
  }
370
+
371
+ return {savedCount}
334
372
  }
335
373
 
336
- async _autoSaveHasManyRelationships({isNewRecord}) {
374
+ _autoSaveHasManyRelationshipsToSave() {
375
+ const relationships = []
376
+
337
377
  for (const relationshipName in this._instanceRelationships) {
338
378
  const instanceRelationship = this._instanceRelationships[relationshipName]
339
379
 
@@ -345,6 +385,31 @@ export default class VelociousDatabaseRecord {
345
385
 
346
386
  if (!Array.isArray(loaded)) loaded = [loaded]
347
387
 
388
+ let useRelationship = false
389
+
390
+ for (const model of loaded) {
391
+ const foreignKey = instanceRelationship.getForeignKey()
392
+
393
+ model.setAttribute(foreignKey, this.id())
394
+
395
+ if (model.isChanged()) {
396
+ useRelationship = true
397
+ continue
398
+ }
399
+ }
400
+
401
+ if (useRelationship) relationships.push(instanceRelationship)
402
+ }
403
+
404
+ return relationships
405
+ }
406
+
407
+ async _autoSaveHasManyRelationships({isNewRecord}) {
408
+ for (const instanceRelationship of this._autoSaveHasManyRelationshipsToSave()) {
409
+ let loaded = instanceRelationship._loaded
410
+
411
+ if (!Array.isArray(loaded)) loaded = [loaded]
412
+
348
413
  for (const model of loaded) {
349
414
  const foreignKey = instanceRelationship.getForeignKey()
350
415
 
@@ -423,6 +488,19 @@ export default class VelociousDatabaseRecord {
423
488
  }
424
489
  }
425
490
 
491
+ static async validates(attributeName, validators) {
492
+ for (const validatorName in validators) {
493
+ const validatorArgs = validators[validatorName]
494
+ const ValidatorClass = this.getValidatorType(validatorName)
495
+ const validator = new ValidatorClass({attributeName, args: validatorArgs})
496
+
497
+ if (!this._validators) this._validators = {}
498
+ if (!(attributeName in this._validators)) this._validators[attributeName] = []
499
+
500
+ this._validators[attributeName].push(validator)
501
+ }
502
+ }
503
+
426
504
  _getTranslatedAttribute(name, locale) {
427
505
  const translation = this.translations().loaded().find((translation) => translation.locale() == locale)
428
506
 
@@ -529,6 +607,10 @@ export default class VelociousDatabaseRecord {
529
607
  return this._newQuery().preload(...args)
530
608
  }
531
609
 
610
+ static select(...args) {
611
+ return this._newQuery().select(...args)
612
+ }
613
+
532
614
  static toArray(...args) {
533
615
  return this._newQuery().toArray(...args)
534
616
  }
@@ -662,14 +744,24 @@ export default class VelociousDatabaseRecord {
662
744
 
663
745
  readAttribute(attributeName) {
664
746
  const attributeNameUnderscore = inflection.underscore(attributeName)
747
+ const column = this.constructor.getColumns().find((column) => column.getName() == attributeNameUnderscore)
748
+ let result
665
749
 
666
- if (attributeNameUnderscore in this._changes) return this._changes[attributeNameUnderscore]
667
-
668
- if (!(attributeNameUnderscore in this._attributes) && this.isPersisted()) {
750
+ if (attributeNameUnderscore in this._changes) {
751
+ result = this._changes[attributeNameUnderscore]
752
+ } else if (attributeNameUnderscore in this._attributes) {
753
+ result = this._attributes[attributeNameUnderscore]
754
+ } else if (this.isPersisted()) {
669
755
  throw new Error(`No such attribute or not selected ${this.constructor.name}#${attributeName}`)
670
756
  }
671
757
 
672
- return this._attributes[attributeNameUnderscore]
758
+ if (column && this.constructor.connection().getType() == "sqlite") {
759
+ if (column.getType() == "date" || column.getType() == "datetime") {
760
+ result = new Date(Date.parse(result))
761
+ }
762
+ }
763
+
764
+ return result
673
765
  }
674
766
 
675
767
  _belongsToChanges() {
@@ -693,7 +785,14 @@ export default class VelociousDatabaseRecord {
693
785
  throw new Error(`No insertSql on ${this.constructor.connection().constructor.name}`)
694
786
  }
695
787
 
788
+ const createdAtColumn = this.constructor.getColumns().find((column) => column.getName() == "created_at")
789
+ const updatedAtColumn = this.constructor.getColumns().find((column) => column.getName() == "updated_at")
696
790
  const data = Object.assign({}, this._belongsToChanges(), this.attributes())
791
+ const currentDate = new Date()
792
+
793
+ if (createdAtColumn) data.created_at = currentDate
794
+ if (updatedAtColumn) data.updated_at = currentDate
795
+
697
796
  const sql = this._connection().insertSql({
698
797
  returnLastInsertedColumnName: this.constructor.primaryKey(),
699
798
  tableName: this._tableName(),
@@ -720,21 +819,25 @@ export default class VelociousDatabaseRecord {
720
819
  }
721
820
 
722
821
  async _updateRecordWithChanges() {
723
- if (!this._hasChanges()) return
724
-
725
822
  const conditions = {}
726
823
 
727
824
  conditions[this.constructor.primaryKey()] = this.id()
728
825
 
729
826
  const changes = Object.assign({}, this._belongsToChanges(), this._changes)
730
-
731
- const sql = this._connection().updateSql({
732
- tableName: this._tableName(),
733
- data: changes,
734
- conditions
735
- })
736
- await this._connection().query(sql)
737
- await this._reloadWithId(this.id())
827
+ const updatedAtColumn = this.constructor.getColumns().find((column) => column.getName() == "updated_at")
828
+ const currentDate = new Date()
829
+
830
+ if (updatedAtColumn) changes.updated_at = currentDate
831
+
832
+ if (Object.keys(changes).length > 0) {
833
+ const sql = this._connection().updateSql({
834
+ tableName: this._tableName(),
835
+ data: changes,
836
+ conditions
837
+ })
838
+ await this._connection().query(sql)
839
+ await this._reloadWithId(this.id())
840
+ }
738
841
  }
739
842
 
740
843
  id = () => this.readAttribute(this.constructor.primaryKey())
@@ -764,9 +867,50 @@ export default class VelociousDatabaseRecord {
764
867
  this._reloadWithId(this.readAttribute("id"))
765
868
  }
766
869
 
870
+ async _runValidations() {
871
+ this._validationErrors = {}
872
+
873
+ const validators = this.constructor._validators
874
+
875
+ if (validators) {
876
+ for (const attributeName in validators) {
877
+ const attributeValidators = validators[attributeName]
878
+
879
+ for (const validator of attributeValidators) {
880
+ await validator.validate({model: this, attributeName})
881
+ }
882
+ }
883
+ }
884
+
885
+ if (Object.keys(this._validationErrors).length > 0) {
886
+ throw new Error(`Validation failed: ${this.fullErrorMessages().join(". ")}`)
887
+ }
888
+ }
889
+
890
+ fullErrorMessages() {
891
+ const validationErrorMessages = []
892
+
893
+ if (this._validationErrors) {
894
+ for (const attributeName in this._validationErrors) {
895
+ for (const validationError of this._validationErrors[attributeName]) {
896
+ const message = `${this.constructor.humanAttributeName(attributeName)} ${validationError.message}`
897
+
898
+ validationErrorMessages.push(message)
899
+ }
900
+ }
901
+ }
902
+
903
+ return validationErrorMessages
904
+ }
905
+
767
906
  async update(attributesToAssign) {
768
907
  if (attributesToAssign) this.assign(attributesToAssign)
769
908
 
770
909
  await this.save()
771
910
  }
772
911
  }
912
+
913
+ VelociousDatabaseRecord.registerValidatorType("presence", ValidatorsPresence)
914
+ VelociousDatabaseRecord.registerValidatorType("uniqueness", ValidatorsUniqueness)
915
+
916
+ export default VelociousDatabaseRecord
@@ -0,0 +1,2 @@
1
+ export default class VelociousDatabaseRecordValidatorsBase {
2
+ }
@@ -0,0 +1,13 @@
1
+ import Base from "./base.js"
2
+
3
+ export default class VelociousDatabaseRecordValidatorsPresence extends Base {
4
+ validate({model, attributeName}) {
5
+ const attributeValue = model.readAttribute(attributeName).trim()
6
+
7
+ if (!attributeValue) {
8
+ if (!(attributeName in model._validationErrors)) model._validationErrors[attributeName] = []
9
+
10
+ model._validationErrors[attributeName].push({type: "presence", message: "can't be blank"})
11
+ }
12
+ }
13
+ }
@@ -0,0 +1,23 @@
1
+ import Base from "./base.js"
2
+ import * as inflection from "inflection"
3
+
4
+ export default class VelociousDatabaseRecordValidatorsUniqueness extends Base {
5
+ async validate({model, attributeName}) {
6
+ const attributeValue = model.readAttribute(attributeName)
7
+ const attributeNameUnderscore = inflection.underscore(attributeName)
8
+ const whereArgs = {}
9
+
10
+ whereArgs[attributeNameUnderscore] = attributeValue
11
+
12
+ const existingRecord = await model.constructor
13
+ .select(model.constructor.primaryKey())
14
+ .where(whereArgs)
15
+ .first()
16
+
17
+ if (existingRecord) {
18
+ if (!(attributeName in model._validationErrors)) model._validationErrors[attributeName] = []
19
+
20
+ model._validationErrors[attributeName].push({type: "uniqueness", message: "has already been taken"})
21
+ }
22
+ }
23
+ }
@@ -43,7 +43,14 @@ export default class TestRunner {
43
43
  }
44
44
  }
45
45
 
46
+ isFailed() {
47
+ return this.failedTests > 0
48
+ }
49
+
46
50
  async run() {
51
+ this.failedTests = 0
52
+ this.successfulTests = 0
53
+
47
54
  await this.importTestFiles()
48
55
  await this.runTests(tests, [], 0)
49
56
  }
@@ -65,7 +72,10 @@ export default class TestRunner {
65
72
 
66
73
  try {
67
74
  await testData.function(testArgs)
75
+ this.successfulTests++
68
76
  } catch (error) {
77
+ this.failedTests++
78
+
69
79
  // console.error(`${leftPadding} Test failed: ${error.message}`)
70
80
  console.error(error.stack)
71
81
  }
@@ -1,97 +0,0 @@
1
- import AsyncTrackedMultiConnection from "../../../../src/database/pool/async-tracked-multi-connection.js"
2
- import Configuration from "../../../../src/configuration.js"
3
- import dummyDirectory from "../../dummy-directory.js"
4
- import fs from "fs/promises"
5
- import InitializerFromRequireContext from "../../../../src/database/initializer-from-require-context.js"
6
- import MssqlDriver from "../../../../src/database/drivers/mssql/index.js"
7
- import MysqlDriver from "../../../../src/database/drivers/mysql/index.js"
8
- import path from "path"
9
- import requireContext from "require-context"
10
-
11
- export default new Configuration({
12
- database: {
13
- development: {
14
- default: {
15
- driver: MysqlDriver,
16
- poolType: AsyncTrackedMultiConnection,
17
- type: "mysql",
18
- host: "mariadb",
19
- username: "dev",
20
- password: "Eid7Eip6iof2weive7yaeshe8eu2Nei4",
21
- database: "velocious_test"
22
- },
23
- mssql: {
24
- driver: MssqlDriver,
25
- poolType: AsyncTrackedMultiConnection,
26
- type: "mssql",
27
- database: "velocious_development",
28
- useDatabase: "velocious_development",
29
- sqlConfig: {
30
- user: "sa",
31
- password: "Super-Secret-Password",
32
- database: "velocious_development",
33
- server: "6.0.0.8",
34
- pool: {
35
- max: 10,
36
- min: 0,
37
- idleTimeoutMillis: 30000
38
- },
39
- options: {
40
- encrypt: true, // for azure
41
- trustServerCertificate: true // change to true for local dev / self-signed certs
42
- }
43
- }
44
- }
45
- },
46
- test: {
47
- default: {
48
- driver: MysqlDriver,
49
- poolType: AsyncTrackedMultiConnection,
50
- type: "mysql",
51
- host: "mariadb",
52
- username: "dev",
53
- password: "Eid7Eip6iof2weive7yaeshe8eu2Nei4",
54
- database: "velocious_test"
55
- },
56
- mssql: {
57
- driver: MssqlDriver,
58
- poolType: AsyncTrackedMultiConnection,
59
- type: "mssql",
60
- database: "velocious_development",
61
- useDatabase: "velocious_development",
62
- sqlConfig: {
63
- user: "sa",
64
- password: "Super-Secret-Password",
65
- database: "velocious_development",
66
- server: "6.0.0.8",
67
- pool: {
68
- max: 10,
69
- min: 0,
70
- idleTimeoutMillis: 30000
71
- },
72
- options: {
73
- encrypt: true, // for azure
74
- trustServerCertificate: true // change to true for local dev / self-signed certs
75
- }
76
- }
77
- }
78
- }
79
- },
80
- debug: false,
81
- directory: dummyDirectory(),
82
- initializeModels: async ({configuration}) => {
83
- const modelsPath = await fs.realpath(`${path.dirname(import.meta.dirname)}/../src/models`)
84
- const requireContextModels = requireContext(modelsPath, true, /^(.+)\.js$/)
85
- const initializerFromRequireContext = new InitializerFromRequireContext({requireContext: requireContextModels})
86
-
87
- await configuration.withConnections(async () => {
88
- await initializerFromRequireContext.initialize({configuration})
89
- })
90
- },
91
- locale: () => "en",
92
- localeFallbacks: {
93
- de: ["de", "en"],
94
- en: ["en", "de"]
95
- },
96
- locales: ["de", "en"]
97
- })