velocious 1.0.40 → 1.0.42

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 (74) hide show
  1. package/README.md +1 -1
  2. package/package.json +2 -1
  3. package/peak_flow.yml +8 -4
  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 +6 -0
  8. package/spec/database/record/destroy-spec.js +6 -3
  9. package/spec/database/record/find-spec.js +3 -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 +10 -6
  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 -2
  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 +34 -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 +27 -6
  38. package/src/controller.js +7 -19
  39. package/src/database/drivers/base-column.js +22 -0
  40. package/src/database/drivers/base-columns-index.js +34 -0
  41. package/src/database/drivers/base-table.js +43 -0
  42. package/src/database/drivers/base.js +44 -16
  43. package/src/database/drivers/mssql/column.js +43 -2
  44. package/src/database/drivers/mssql/columns-index.js +9 -0
  45. package/src/database/drivers/mssql/index.js +26 -14
  46. package/src/database/drivers/mssql/table.js +16 -1
  47. package/src/database/drivers/mysql/column.js +47 -2
  48. package/src/database/drivers/mysql/columns-index.js +10 -0
  49. package/src/database/drivers/mysql/index.js +5 -8
  50. package/src/database/drivers/mysql/table.js +3 -1
  51. package/src/database/drivers/pgsql/column.js +37 -2
  52. package/src/database/drivers/pgsql/columns-index.js +4 -0
  53. package/src/database/drivers/pgsql/index.js +6 -5
  54. package/src/database/drivers/pgsql/table.js +3 -1
  55. package/src/database/drivers/sqlite/base.js +6 -4
  56. package/src/database/drivers/sqlite/column.js +46 -2
  57. package/src/database/drivers/sqlite/columns-index.js +22 -0
  58. package/src/database/drivers/sqlite/connection-sql-js.js +1 -1
  59. package/src/database/drivers/sqlite/table.js +3 -1
  60. package/src/database/migrator.js +27 -7
  61. package/src/database/query/create-index-base.js +10 -1
  62. package/src/database/query/create-table-base.js +56 -2
  63. package/src/database/query/drop-table-base.js +8 -2
  64. package/src/database/table-data/index.js +2 -1
  65. package/src/database/use-database.js +1 -1
  66. package/src/http-server/client/request-buffer/index.js +9 -5
  67. package/src/http-server/client/request.js +6 -6
  68. package/src/routes/base-route.js +11 -0
  69. package/src/routes/namespace-route.js +24 -0
  70. package/src/routes/resolver.js +1 -1
  71. package/src/templates/configuration.js +1 -1
  72. package/src/testing/test-runner.js +86 -26
  73. package/src/testing/test.js +155 -7
  74. package/src/utils/with-tracked-stack.js +5 -3
@@ -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()
@@ -166,12 +166,16 @@ export default class RequestBuffer {
166
166
  this.multiPartyFormData = true
167
167
  this.setState("multi-part-form-data")
168
168
  } else {
169
- if (!this.contentLength) throw new Error("Content length hasn't been set")
170
-
171
- this.postBodyBuffer = new ArrayBuffer(this.contentLength)
172
- this.postBodyChars = new Uint8Array(this.postBodyBuffer)
169
+ if (this.contentLength === 0) {
170
+ this.completeRequest()
171
+ } else if (!this.contentLength) {
172
+ throw new Error("Content length hasn't been set")
173
+ } else {
174
+ this.postBodyBuffer = new ArrayBuffer(this.contentLength)
175
+ this.postBodyChars = new Uint8Array(this.postBodyBuffer)
173
176
 
174
- this.setState("post-body")
177
+ this.setState("post-body")
178
+ }
175
179
  }
176
180
  } else {
177
181
  throw new Error(`Unknown HTTP method: ${this.httpMethod}`)
@@ -7,13 +7,13 @@ export default class VelociousHttpServerClientRequest {
7
7
  this.requestParser = new RequestParser({configuration})
8
8
  }
9
9
 
10
- baseURL = () => `${this.protocol()}://${this.hostWithPort()}`
11
- feed = (data) => this.requestParser.feed(data)
12
- header = (headerName) => this.requestParser.requestBuffer.getHeader(headerName)?.value
13
- httpMethod = () => this.requestParser.getHttpMethod()
14
- host = () => this.requestParser.getHost()
10
+ baseURL() { return `${this.protocol()}://${this.hostWithPort()}` }
11
+ feed(data) { return this.requestParser.feed(data) }
12
+ header(headerName) { return this.requestParser.requestBuffer.getHeader(headerName)?.value }
13
+ httpMethod() { return this.requestParser.getHttpMethod() }
14
+ host() { return this.requestParser.getHost() }
15
15
 
16
- hostWithPort = () => {
16
+ hostWithPort() {
17
17
  const port = this.port()
18
18
  const protocol = this.protocol()
19
19
  let hostWithPort = `${this.host()}`
@@ -1,4 +1,5 @@
1
1
  import GetRoute from "./get-route.js"
2
+ import NamespaceRoute from "./namespace-route.js"
2
3
  import PostRoute from "./post-route.js"
3
4
  import ResourceRoute from "./resource-route.js"
4
5
 
@@ -20,6 +21,16 @@ export function initBaseRoute() {
20
21
  throw new Error(`No 'matchWithPath' implemented on ${this.constructor.name}`)
21
22
  }
22
23
 
24
+ namespace(name, callback) {
25
+ const route = new NamespaceRoute({name})
26
+
27
+ this.routes.push(route)
28
+
29
+ if (callback) {
30
+ callback(route)
31
+ }
32
+ }
33
+
23
34
  post(name, args) {
24
35
  const route = new PostRoute({name, args})
25
36
 
@@ -0,0 +1,24 @@
1
+ import BaseRoute, {initBaseRoute} from "./base-route.js"
2
+ import escapeStringRegexp from "escape-string-regexp"
3
+
4
+ initBaseRoute()
5
+
6
+ export default class VelociousRouteNamespaceRoute extends BaseRoute {
7
+ constructor({name}) {
8
+ super()
9
+ this.name = name
10
+ this.regExp = new RegExp(`^(${escapeStringRegexp(name)})(.*)$`)
11
+ }
12
+
13
+ matchWithPath({params, path}) {
14
+ const match = path.match(this.regExp)
15
+
16
+ if (match) {
17
+ const [_beginnigSlash, _matchedName, restPath] = match
18
+
19
+ params.controller = this.name
20
+
21
+ return {restPath}
22
+ }
23
+ }
24
+ }
@@ -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