velocious 1.0.39 → 1.0.41

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 (71) hide show
  1. package/README.md +1 -1
  2. package/package.json +2 -1
  3. package/peak_flow.yml +14 -6
  4. package/spec/cli/commands/db/create-spec.js +1 -0
  5. package/spec/cli/commands/db/migrate-spec.js +28 -11
  6. package/spec/cli/commands/test/test-files-finder-spec.js +5 -4
  7. package/spec/database/record/create-spec.js +9 -0
  8. package/spec/database/record/destroy-spec.js +6 -3
  9. package/spec/database/record/find-spec.js +4 -1
  10. package/spec/database/record/query-spec.js +7 -3
  11. package/spec/database/record/translation-fallbacks-spec.js +1 -1
  12. package/spec/database/record/update-spec.js +2 -1
  13. package/spec/database/record/validations-spec.js +16 -4
  14. package/spec/database/transactions-spec.js +7 -5
  15. package/spec/dummy/index.js +18 -29
  16. package/spec/dummy/src/config/configuration.example.js +3 -2
  17. package/spec/dummy/src/config/configuration.peakflow.mariadb.js +3 -7
  18. package/spec/dummy/src/config/configuration.peakflow.mssql.js +3 -12
  19. package/spec/dummy/src/config/configuration.peakflow.pgsql.js +3 -7
  20. package/spec/dummy/src/config/configuration.peakflow.sqlite.js +3 -7
  21. package/spec/dummy/src/config/testing.js +62 -0
  22. package/spec/dummy/src/database/migrations/20250912183605-create-users.js +15 -0
  23. package/spec/dummy/src/database/migrations/20250912183606-create-authentication-tokens.js +15 -0
  24. package/spec/dummy/src/models/authentication-token.js +8 -0
  25. package/spec/dummy/src/models/task.js +2 -2
  26. package/spec/dummy/src/models/user.js +15 -0
  27. package/spec/dummy/src/routes/projects/controller.js +7 -1
  28. package/spec/http-server/client-spec.js +1 -1
  29. package/spec/http-server/get-spec.js +1 -1
  30. package/spec/http-server/post-spec.js +21 -8
  31. package/spec/http-server/root-get-spec.js +1 -1
  32. package/src/cli/commands/db/create.js +11 -8
  33. package/src/cli/commands/db/drop.js +19 -0
  34. package/src/cli/commands/db/migrate.js +1 -1
  35. package/src/cli/commands/db/reset.js +7 -1
  36. package/src/cli/commands/test.js +10 -4
  37. package/src/configuration.js +26 -3
  38. package/src/database/drivers/base-column.js +22 -0
  39. package/src/database/drivers/base-columns-index.js +34 -0
  40. package/src/database/drivers/base-table.js +43 -0
  41. package/src/database/drivers/base.js +12 -16
  42. package/src/database/drivers/mssql/column.js +43 -2
  43. package/src/database/drivers/mssql/columns-index.js +9 -0
  44. package/src/database/drivers/mssql/index.js +26 -14
  45. package/src/database/drivers/mssql/table.js +16 -1
  46. package/src/database/drivers/mysql/column.js +47 -2
  47. package/src/database/drivers/mysql/columns-index.js +10 -0
  48. package/src/database/drivers/mysql/index.js +5 -8
  49. package/src/database/drivers/mysql/table.js +3 -1
  50. package/src/database/drivers/pgsql/column.js +37 -2
  51. package/src/database/drivers/pgsql/columns-index.js +4 -0
  52. package/src/database/drivers/pgsql/index.js +6 -5
  53. package/src/database/drivers/pgsql/table.js +3 -1
  54. package/src/database/drivers/sqlite/base.js +6 -4
  55. package/src/database/drivers/sqlite/column.js +46 -2
  56. package/src/database/drivers/sqlite/columns-index.js +22 -0
  57. package/src/database/drivers/sqlite/connection-sql-js.js +1 -1
  58. package/src/database/drivers/sqlite/table.js +3 -1
  59. package/src/database/migrator.js +27 -7
  60. package/src/database/query/create-index-base.js +10 -1
  61. package/src/database/query/create-table-base.js +56 -2
  62. package/src/database/query/drop-table-base.js +8 -2
  63. package/src/database/record/index.js +16 -5
  64. package/src/database/record/validators/presence.js +1 -1
  65. package/src/database/table-data/index.js +2 -1
  66. package/src/database/use-database.js +1 -1
  67. package/src/routes/resolver.js +1 -1
  68. package/src/templates/configuration.js +1 -1
  69. package/src/testing/test-runner.js +86 -26
  70. package/src/testing/test.js +155 -7
  71. package/src/utils/with-tracked-stack.js +5 -3
@@ -1,12 +1,14 @@
1
+ import {digs} from "diggerize"
1
2
  import QueryBase from "./base.js"
2
3
  import restArgsError from "../../utils/rest-args-error.js"
3
4
 
4
5
  export default class VelociousDatabaseQueryDropTableBase extends QueryBase {
5
- constructor({driver, ifExists, options, tableName, ...restArgs}) {
6
+ constructor({cascade, driver, ifExists, options, tableName, ...restArgs}) {
6
7
  super({driver, options})
7
8
 
8
9
  restArgsError(restArgs)
9
10
 
11
+ this.cascade = cascade
10
12
  this.ifExists = ifExists
11
13
  this.tableName = tableName
12
14
  }
@@ -14,7 +16,7 @@ export default class VelociousDatabaseQueryDropTableBase extends QueryBase {
14
16
  toSql() {
15
17
  const databaseType = this.getDatabaseType()
16
18
  const options = this.getOptions()
17
- const {ifExists, tableName} = this
19
+ const {cascade, ifExists, tableName} = digs(this, "cascade", "ifExists", "tableName")
18
20
  const sqls = []
19
21
  let sql = ""
20
22
 
@@ -28,6 +30,10 @@ export default class VelociousDatabaseQueryDropTableBase extends QueryBase {
28
30
 
29
31
  sql += ` ${options.quoteTableName(tableName)}`
30
32
 
33
+ if (cascade && databaseType == "pgsql") {
34
+ sql += " cascade"
35
+ }
36
+
31
37
  if (databaseType == "mssql" && ifExists) {
32
38
  sql += " END"
33
39
  }
@@ -330,7 +330,13 @@ class VelociousDatabaseRecord {
330
330
  return record
331
331
  }
332
332
 
333
+ static setPrimaryKey(primaryKey) {
334
+ this._primaryKey = primaryKey
335
+ }
336
+
333
337
  static primaryKey() {
338
+ if (this._primaryKey) return this._primaryKey
339
+
334
340
  return "id"
335
341
  }
336
342
 
@@ -762,13 +768,18 @@ class VelociousDatabaseRecord {
762
768
 
763
769
  readAttribute(attributeName) {
764
770
  const attributeNameUnderscore = inflection.underscore(attributeName)
765
- const column = this.constructor.getColumns().find((column) => column.getName() == attributeNameUnderscore)
771
+
772
+ return this.readColumn(attributeNameUnderscore)
773
+ }
774
+
775
+ readColumn(attributeName) {
776
+ const column = this.constructor.getColumns().find((column) => column.getName() == attributeName)
766
777
  let result
767
778
 
768
- if (attributeNameUnderscore in this._changes) {
769
- result = this._changes[attributeNameUnderscore]
770
- } else if (attributeNameUnderscore in this._attributes) {
771
- result = this._attributes[attributeNameUnderscore]
779
+ if (attributeName in this._changes) {
780
+ result = this._changes[attributeName]
781
+ } else if (attributeName in this._attributes) {
782
+ result = this._attributes[attributeName]
772
783
  } else if (this.isPersisted()) {
773
784
  throw new Error(`No such attribute or not selected ${this.constructor.name}#${attributeName}`)
774
785
  }
@@ -2,7 +2,7 @@ import Base from "./base.js"
2
2
 
3
3
  export default class VelociousDatabaseRecordValidatorsPresence extends Base {
4
4
  validate({model, attributeName}) {
5
- const attributeValue = model.readAttribute(attributeName).trim()
5
+ const attributeValue = model.readAttribute(attributeName)?.trim()
6
6
 
7
7
  if (!attributeValue) {
8
8
  if (!(attributeName in model._validationErrors)) model._validationErrors[attributeName] = []
@@ -3,7 +3,7 @@ import restArgsError from "../../utils/rest-args-error.js"
3
3
  class TableColumn {
4
4
  constructor(name, args) {
5
5
  if (args) {
6
- const {autoIncrement, default: columnDefault, foreignKey, maxLength, name, null: argsNull, primaryKey, type, ...restArgs} = args
6
+ const {autoIncrement, default: columnDefault, foreignKey, index, maxLength, name, null: argsNull, primaryKey, type, ...restArgs} = args
7
7
 
8
8
  restArgsError(restArgs)
9
9
  }
@@ -15,6 +15,7 @@ class TableColumn {
15
15
  getAutoIncrement = () => this.args?.autoIncrement
16
16
  getDefault = () => this.args?.default
17
17
  getForeignKey = () => this.args?.foreignKey
18
+ getIndex = () => this.args?.index
18
19
  getMaxLength = () => this.args?.maxLength
19
20
  getName = () => this.name
20
21
  getNull = () => this.args?.null
@@ -14,7 +14,7 @@ const loadMigrations = function loadMigrations({migrationsRequireContext, ...res
14
14
  instance.running = true
15
15
 
16
16
  try {
17
- await Configuration.current().withConnections(async () => {
17
+ await Configuration.current().ensureConnections(async () => {
18
18
  const migrator = new Migrator({configuration: Configuration.current()})
19
19
 
20
20
  await migrator.prepare()
@@ -59,7 +59,7 @@ export default class VelociousRoutesResolver {
59
59
  throw new Error(`Missing action on controller: ${controller}#${action}`)
60
60
  }
61
61
 
62
- await this.configuration.withConnections(async () => {
62
+ await this.configuration.ensureConnections(async () => {
63
63
  await controllerInstance._runBeforeCallbacks()
64
64
  await controllerInstance[action]()
65
65
  })
@@ -43,7 +43,7 @@ export default new Configuration({
43
43
  const requireContextModels = requireContext(modelsPath, true, /^(.+)\.js$/)
44
44
  const initializerFromRequireContext = new InitializerFromRequireContext({requireContext: requireContextModels})
45
45
 
46
- await configuration.withConnections(async () => {
46
+ await configuration.ensureConnections(async () => {
47
47
  await initializerFromRequireContext.initialize({configuration})
48
48
  })
49
49
  },
@@ -45,45 +45,87 @@ export default class TestRunner {
45
45
  }
46
46
 
47
47
  isFailed() {
48
- return this.failedTests > 0
48
+ return this._failedTests > 0
49
49
  }
50
50
 
51
- async run() {
52
- this.failedTests = 0
53
- this.successfulTests = 0
51
+ getFailedTests() {
52
+ return this._failedTests
53
+ }
54
+
55
+ getSuccessfulTests() {
56
+ return this._successfulTests
57
+ }
58
+
59
+ getTestsCount() {
60
+ return this._testsCount
61
+ }
62
+
63
+ async prepare() {
64
+ this._failedTests = 0
65
+ this._successfulTests = 0
66
+ this._testsCount = 0
54
67
  await this.importTestFiles()
55
- this.onlyFocussed = this.areAnyTestsFocussed(tests)
56
- await this.runTests(tests, [], 0)
68
+ await this.analyzeTests(tests)
69
+ this._onlyFocussed = this.anyTestsFocussed
70
+
71
+ const testingConfigPath = this.configuration.getTesting()
72
+
73
+ if (testingConfigPath) {
74
+ await import(testingConfigPath)
75
+ }
76
+ }
77
+
78
+ async run() {
79
+ await this.configuration.ensureConnections(async () => {
80
+ await this.runTests({
81
+ afterEaches: [],
82
+ beforeEaches: [],
83
+ tests,
84
+ descriptions: [],
85
+ indentLevel: 0
86
+ })
87
+ })
57
88
  }
58
89
 
59
- areAnyTestsFocussed(tests) {
90
+ analyzeTests(tests) {
91
+ let anyTestsFocussedFound = false
92
+
60
93
  for (const testDescription in tests.tests) {
61
94
  const testData = tests.tests[testDescription]
62
95
  const testArgs = Object.assign({}, testData.args)
63
96
 
97
+ this._testsCount++
98
+
64
99
  if (testArgs.focus) {
65
- return true
100
+ anyTestsFocussedFound = true
101
+ this.anyTestsFocussed = true
66
102
  }
67
103
  }
68
104
 
69
105
  for (const subDescription in tests.subs) {
70
106
  const subTest = tests.subs[subDescription]
71
- const result = this.areAnyTestsFocussed(subTest)
107
+ const {anyTestsFocussed} = this.analyzeTests(subTest)
108
+
109
+ if (anyTestsFocussed) {
110
+ anyTestsFocussedFound = true
111
+ }
72
112
 
73
- if (result) return true
113
+ subTest.anyTestsFocussed = anyTestsFocussed
74
114
  }
75
115
 
76
- return false
116
+ return {anyTestsFocussed: anyTestsFocussedFound}
77
117
  }
78
118
 
79
- async runTests(tests, descriptions, indentLevel) {
119
+ async runTests({afterEaches, beforeEaches, tests, descriptions, indentLevel}) {
80
120
  const leftPadding = " ".repeat(indentLevel * 2)
121
+ const newAfterEaches = [...afterEaches, ...tests.afterEaches]
122
+ const newBeforeEaches = [...beforeEaches, ...tests.beforeEaches]
81
123
 
82
124
  for (const testDescription in tests.tests) {
83
125
  const testData = tests.tests[testDescription]
84
126
  const testArgs = Object.assign({}, testData.args)
85
127
 
86
- if (this.onlyFocussed && !testArgs.focus) continue
128
+ if (this._onlyFocussed && !testArgs.focus) continue
87
129
 
88
130
  if (testArgs.type == "request") {
89
131
  testArgs.application = await this.application()
@@ -93,26 +135,44 @@ export default class TestRunner {
93
135
  console.log(`${leftPadding}it ${testDescription}`)
94
136
 
95
137
  try {
138
+ for (const beforeEachData of newBeforeEaches) {
139
+ await beforeEachData.callback({testArgs, testData})
140
+ }
141
+
96
142
  await testData.function(testArgs)
97
- this.successfulTests++
143
+ this._successfulTests++
98
144
  } catch (error) {
99
- this.failedTests++
145
+ this._failedTests++
100
146
 
101
147
  // console.error(`${leftPadding} Test failed: ${error.message}`)
102
- console.error(addTrackedStackToError(error))
103
- }
104
- }
148
+ addTrackedStackToError(error)
105
149
 
106
- await this.configuration.withConnections(async () => {
107
- for (const subDescription in tests.subs) {
108
- const subTest = tests.subs[subDescription]
109
- const newDecriptions = descriptions.concat([subDescription])
150
+ const stackLines = error.stack.split("\n")
110
151
 
111
- if (!this.onlyFocussed || this.areAnyTestsFocussed(subTest)) {
112
- console.log(`${leftPadding}${subDescription}`)
113
- await this.runTests(subTest, newDecriptions, indentLevel + 1)
152
+ for (const stackLine of stackLines) {
153
+ console.error(`${leftPadding} ${stackLine}`)
154
+ }
155
+ } finally {
156
+ for (const afterEachData of newAfterEaches) {
157
+ await afterEachData.callback({testArgs, testData})
114
158
  }
115
159
  }
116
- })
160
+ }
161
+
162
+ for (const subDescription in tests.subs) {
163
+ const subTest = tests.subs[subDescription]
164
+ const newDecriptions = descriptions.concat([subDescription])
165
+
166
+ if (!this._onlyFocussed || subTest.anyTestsFocussed) {
167
+ console.log(`${leftPadding}${subDescription}`)
168
+ await this.runTests({
169
+ afterEaches: newAfterEaches,
170
+ beforeEaches: newBeforeEaches,
171
+ tests: subTest,
172
+ descriptions: newDecriptions,
173
+ indentLevel: indentLevel + 1
174
+ })
175
+ }
176
+ }
117
177
  }
118
178
  }
@@ -1,13 +1,28 @@
1
+ import {anythingDifferent} from "set-state-compare/src/diff-utils.js"
1
2
  import restArgsError from "../utils/rest-args-error.js"
2
3
 
3
4
  const tests = {
5
+ afterEaches: [],
4
6
  args: {},
7
+ beforeEaches: [],
5
8
  subs: {},
6
9
  tests: {}
7
10
  }
8
11
 
9
12
  let currentPath = [tests]
10
13
 
14
+ function beforeEach(callback) {
15
+ const currentTest = currentPath[currentPath.length - 1]
16
+
17
+ currentTest.beforeEaches.push({callback})
18
+ }
19
+
20
+ function afterEach(callback) {
21
+ const currentTest = currentPath[currentPath.length - 1]
22
+
23
+ currentTest.afterEaches.push({callback})
24
+ }
25
+
11
26
  class ExpectToChange {
12
27
  constructor({changeCallback, expect, ...restArgs}) {
13
28
  restArgsError(restArgs)
@@ -45,7 +60,61 @@ class Expect {
45
60
  this.expectations = []
46
61
  }
47
62
 
63
+ andChange(...args) {
64
+ return this.toChange(...args)
65
+ }
66
+
67
+ get not() {
68
+ this._not = true
69
+
70
+ return this
71
+ }
72
+
73
+ toBe(result) {
74
+ if (this._not) {
75
+ if (this._object === result) {
76
+ throw new Error(`${this._object} was unexpected not to be ${result}`)
77
+ }
78
+ } else {
79
+ if (this._object !== result) {
80
+ throw new Error(`${this._object} wasn't expected be ${result}`)
81
+ }
82
+ }
83
+ }
84
+
85
+ toBeDefined() {
86
+ if (this._not) {
87
+ if (this._object !== undefined) {
88
+ throw new Error(`${this._object} wasn´t expected to be defined`)
89
+ }
90
+ } else {
91
+ if (this._object === undefined) {
92
+ throw new Error(`${this._object} wasn't expected be undefined`)
93
+ }
94
+ }
95
+ }
96
+
97
+ toBeInstanceOf(klass) {
98
+ if (!(this._object instanceof klass)) {
99
+ throw new Error(`Expected ${this._object?.constructor?.name || "null"} to be a ${klass.name} but it wasn't`)
100
+ }
101
+ }
102
+
103
+ toBeFalse() {
104
+ this.toBe(false)
105
+ }
106
+
107
+ toBeUndefined() {
108
+ this.toBe(undefined)
109
+ }
110
+
111
+ toBeTrue() {
112
+ this.toBe(true)
113
+ }
114
+
48
115
  toChange(changeCallback) {
116
+ if (this._not) throw new Error("not stub")
117
+
49
118
  const expectToChange = new ExpectToChange({changeCallback, expect: this})
50
119
 
51
120
  this.expectations.push(expectToChange)
@@ -53,13 +122,81 @@ class Expect {
53
122
  return expectToChange
54
123
  }
55
124
 
56
- andChange(...args) {
57
- return this.toChange(...args)
125
+ toContain(valueToContain) {
126
+ if (this._not) throw new Error("not stub")
127
+
128
+ if (!this._object.includes(valueToContain)) {
129
+ throw new Error(`${this._object} doesn't contain ${valueToContain}`)
130
+ }
58
131
  }
59
132
 
60
133
  toEqual(result) {
61
- if (this._object != result) {
62
- throw new Error(`${this._object} wasn't equal to ${result}`)
134
+ if (this._not) {
135
+ if (typeof this._object == "object" && typeof result == "object") {
136
+ if (!anythingDifferent(this._object, result)) {
137
+ throw new Error(`${this._object} was unexpected equal to ${result}`)
138
+ }
139
+ } else {
140
+ if (this._object == result) {
141
+ throw new Error(`${this._object} was unexpected equal to ${result}`)
142
+ }
143
+ }
144
+ } else {
145
+ if (typeof this._object == "object" && typeof result == "object") {
146
+ if (anythingDifferent(this._object, result)) {
147
+ throw new Error(`${JSON.stringify(this._object)} wasn't equal to ${JSON.stringify(result)}`)
148
+ }
149
+ } else {
150
+ if (this._object != result) {
151
+ throw new Error(`${this._object} wasn't equal to ${result}`)
152
+ }
153
+ }
154
+ }
155
+ }
156
+
157
+ toMatch(regex) {
158
+ const match = this._object.match(regex)
159
+
160
+ if (this._not) {
161
+ if (match) {
162
+ throw new Error(`${this._object} shouldn't match ${regex}`)
163
+ }
164
+ } else {
165
+ if (!match) {
166
+ throw new Error(`${this._object} didn't match ${regex}`)
167
+ }
168
+ }
169
+ }
170
+
171
+ async toThrowError(expectedError) {
172
+ if (this._not) throw new Error("not stub")
173
+
174
+ let failedError
175
+
176
+ try {
177
+ await this._object()
178
+ } catch (error) {
179
+ failedError = error
180
+ }
181
+
182
+ if (!failedError) throw new Error("Expected to fail but didn't")
183
+
184
+ let expectedErrorMessage, failedErrorMessage
185
+
186
+ if (typeof failedError == "string") {
187
+ failedErrorMessage = failedError
188
+ } else {
189
+ failedErrorMessage = failedError.message
190
+ }
191
+
192
+ if (typeof expectedError == "string") {
193
+ expectedErrorMessage = expectedError
194
+ } else {
195
+ expectedErrorMessage = expectedError.message
196
+ }
197
+
198
+ if (failedErrorMessage != expectedErrorMessage) {
199
+ throw new Error(`Expected to fail with '${expectedErrorMessage}' but failed with '${failedErrorMessage}'`)
63
200
  }
64
201
  }
65
202
 
@@ -82,6 +219,8 @@ class Expect {
82
219
  }
83
220
 
84
221
  toHaveAttributes(result) {
222
+ if (this._not) throw new Error("not stub")
223
+
85
224
  const differences = {}
86
225
 
87
226
  for (const key in result) {
@@ -93,8 +232,9 @@ class Expect {
93
232
  }
94
233
  }
95
234
 
96
- if (Object.keys(differences).length > 0)
97
- throw new Error(`Object had differet values: ${JSON.stringify(differences)}`)
235
+ if (Object.keys(differences).length > 0) {
236
+ throw new Error(`Object had differet values: ${JSON.stringify(differences)}`)
237
+ }
98
238
  }
99
239
  }
100
240
 
@@ -118,7 +258,7 @@ async function describe(description, arg1, arg2) {
118
258
  throw new Error(`Duplicate test description: ${description}`)
119
259
  }
120
260
 
121
- const newTestData = {args: newTestArgs, subs: {}, tests: {}}
261
+ const newTestData = {afterEaches: [], args: newTestArgs, beforeEaches: [], subs: {}, tests: {}}
122
262
 
123
263
  currentTest.subs[description] = newTestData
124
264
  currentPath.push(newTestData)
@@ -169,4 +309,12 @@ function fit(description, arg1, arg2) {
169
309
  return it(description, testArgs, testFunction)
170
310
  }
171
311
 
312
+ // Make the methods global so they can be used in test files
313
+ globalThis.afterEach = afterEach
314
+ globalThis.beforeEach = beforeEach
315
+ globalThis.describe = describe
316
+ globalThis.expect = expect
317
+ globalThis.it = it
318
+ globalThis.fit = fit
319
+
172
320
  export {describe, expect, fit, it, tests}
@@ -38,16 +38,18 @@ async function withTrackedStack(arg1, arg2) {
38
38
  if (!asyncLocalStorage) return await callback()
39
39
 
40
40
  const parentStacks = asyncLocalStorage.getStore() || []
41
- const additionalStackLines = [" [WITH TRACKED STACK]"]
41
+ const additionalStackLines = []
42
42
  const currentStackLines = stack.split("\n")
43
43
 
44
+ currentStackLines[0] = " [WITH TRACKED STACK]"
45
+
44
46
  for (let i = currentStackLines.length; i >= 0; i--) {
45
47
  const stackLine = currentStackLines[i]
46
48
 
49
+ additionalStackLines.unshift(stackLine)
50
+
47
51
  if (stackLine == " [WITH TRACKED STACK]") {
48
52
  break
49
- } else {
50
- additionalStackLines.unshift(stackLine)
51
53
  }
52
54
  }
53
55