velocious 1.0.23 → 1.0.25

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.
package/README.md CHANGED
@@ -32,13 +32,90 @@ npx velocious db:migrate
32
32
  # Models
33
33
 
34
34
  ```bash
35
- npx velocious g:model Option
35
+ npx velocious g:model Task
36
+ ```
37
+
38
+ # Migrations
39
+ ```bash
40
+ npx velocious g:migration create-tasks
41
+ ```
42
+
43
+ ```js
44
+ import Migration from "velocious/src/database/migration/index.js"
45
+
46
+ export default class CreateEvents extends Migration {
47
+ async up() {
48
+ await this.createTable("tasks", (t) => {
49
+ t.timestamps()
50
+ })
51
+
52
+ await this.createTable("task_translations", (t) => {
53
+ t.references("task", {foreignKey: true, null: false})
54
+ t.string("locale", {null: false})
55
+ t.string("name")
56
+ t.timestamps()
57
+ })
58
+
59
+ await this.addIndex("task_translations", ["task_id", "locale"], {unique: true})
60
+ }
61
+
62
+ async down() {
63
+ await this.dropTable("task_translations")
64
+ await this.dropTable("tasks")
65
+ }
66
+ }
67
+ ```
68
+
69
+ # Querying
70
+
71
+ ```js
72
+ import {Task} from "@/src/models/task"
73
+
74
+ const tasks = await Task
75
+ .preload({project: {account: true}})
76
+ .where({projects: {public: true}})
77
+ .toArray()
36
78
  ```
37
79
 
38
80
  # Testing
39
81
 
82
+ If you are using Velocious for an app, Velocious has a built-in testing framework. You can run your tests like this:
40
83
  ```bash
41
- npm test
84
+ npx velocious test
85
+ ```
86
+
87
+ If you are developing on Velocious, you can run the tests with:
88
+
89
+ ```bash
90
+ npm run test
91
+ ```
92
+
93
+ # Writing a require test
94
+
95
+ First create a test file under something like the following path 'src/routes/accounts/create-test.js' with something like the following content:
96
+
97
+ ```js
98
+ import {describe, expect, it} from "velocious/src/testing/test.js"
99
+ import Account from "../../models/account.js"
100
+
101
+ await describe("accounts - create", {type: "request"}, async () => {
102
+ it("creates an account", async ({client}) => {
103
+ const response = await client.post("/accounts", {account: {name: "My event company"}})
104
+
105
+ expect(response.statusCode()).toEqual(200)
106
+ expect(response.contentType()).toEqual("application/json")
107
+
108
+ const data = JSON.parse(response.body())
109
+
110
+ expect(data.status).toEqual("success")
111
+
112
+ const createdAccount = await Account.last()
113
+
114
+ expect(createdAccount).toHaveAttributes({
115
+ name: "My event company"
116
+ })
117
+ })
118
+ })
42
119
  ```
43
120
 
44
121
  # Running a server
package/bin/velocious.js CHANGED
@@ -3,7 +3,23 @@
3
3
  import Cli from "../src/cli/index.js"
4
4
 
5
5
  const processArgs = process.argv.slice(2)
6
- const cli = new Cli({processArgs})
6
+ const parsedProcessArgs = {}
7
+
8
+ for (let i = 0; i < processArgs.length; i++) {
9
+ const processArg = processArgs[i]
10
+ const singleLetterArgMatch = processArg.match(/^-([a-z])$/)
11
+ const multiLetterArgMatch = processArg.match(/^--([a-z]+)$/)
12
+
13
+ if (singleLetterArgMatch) {
14
+ parsedProcessArgs[singleLetterArgMatch[1]] = processArgs[i + 1]
15
+ i++
16
+ } else if (multiLetterArgMatch) {
17
+ parsedProcessArgs[multiLetterArgMatch[1]] = processArgs[i + 1]
18
+ i++
19
+ }
20
+ }
21
+
22
+ const cli = new Cli({parsedProcessArgs, processArgs})
7
23
 
8
24
  await cli.execute()
9
25
 
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "velocious": "bin/velocious.js"
4
4
  },
5
5
  "name": "velocious",
6
- "version": "1.0.23",
6
+ "version": "1.0.25",
7
7
  "main": "index.js",
8
8
  "scripts": {
9
9
  "test": "jasmine",
@@ -1,4 +1,4 @@
1
- import TestFilesFinder from "../../../../src/cli/commands/test/test-files-finder.js"
1
+ import TestFilesFinder from "../../../../src/testing/test-files-finder.js"
2
2
 
3
3
  describe("Cli - Commands - test - TestFilesFinder", () => {
4
4
  it("finds the correct test files", async () => {
@@ -7,7 +7,20 @@ describe("HttpServer", () => {
7
7
  const response = await fetch("http://localhost:3006/tasks")
8
8
  const text = await response.text()
9
9
 
10
+ expect(response.status).toEqual(200)
11
+ expect(response.statusText).toEqual("OK")
10
12
  expect(text).toEqual("1, 2, 3, 4, 5\n")
11
13
  })
12
14
  })
15
+
16
+ it("returns a 404 error when a collection action isnt found", async () => {
17
+ await Dummy.run(async () => {
18
+ const response = await fetch("http://localhost:3006/tasks/doesnt-exist")
19
+ const text = await response.text()
20
+
21
+ expect(response.status).toEqual(404)
22
+ expect(response.statusText).toEqual("Not Found")
23
+ expect(text).toEqual("Not found!\n")
24
+ })
25
+ })
13
26
  })
@@ -43,6 +43,7 @@ export default class VelociousApplication {
43
43
  logger(this, `Starting server on port ${port}`)
44
44
 
45
45
  this.httpServer = new HttpServer({configuration, port})
46
+ this.httpServer.events.on("close", this.onHttpServerClose)
46
47
 
47
48
  await this.httpServer.start()
48
49
  }
@@ -52,4 +53,18 @@ export default class VelociousApplication {
52
53
 
53
54
  await this.httpServer.stop()
54
55
  }
56
+
57
+ onHttpServerClose = () => {
58
+ logger(this, "HTTP server closed")
59
+
60
+ if (this.waitResolve) {
61
+ this.waitResolve()
62
+ }
63
+ }
64
+
65
+ wait() {
66
+ return new Promise((resolve) => {
67
+ this.waitResolve = resolve
68
+ })
69
+ }
55
70
  }
@@ -5,14 +5,13 @@ export default class VelociousCliCommandsServer extends BaseCommand{
5
5
  async execute() {
6
6
  this.databasePool = this.configuration.getDatabasePool()
7
7
  this.newConfiguration = Object.assign({}, this.databasePool.getConfiguration())
8
-
9
- if (this.args.testing) this.result = []
10
-
11
8
  this.databaseConnection = await this.databasePool.spawnConnectionWithConfiguration(this.newConfiguration)
9
+
12
10
  await this.databaseConnection.connect()
13
11
 
14
- const host = "0.0.0.0"
15
- const port = 3006
12
+ const {parsedProcessArgs} = this.args
13
+ const host = parsedProcessArgs.h || parsedProcessArgs.host || "127.0.0.1"
14
+ const port = parsedProcessArgs.p || parsedProcessArgs.port || 3006
16
15
  const application = new Application({
17
16
  configuration: this.configuration,
18
17
  httpServer: {
@@ -23,7 +22,7 @@ export default class VelociousCliCommandsServer extends BaseCommand{
23
22
 
24
23
  await application.initialize()
25
24
  await application.startHttpServer()
26
-
27
25
  console.log(`Started Velocious HTTP server on ${host}:${port}`)
26
+ await application.wait()
28
27
  }
29
28
  }
@@ -1,13 +1,12 @@
1
- import BaseCommand from "../../base-command.js"
2
- import TestFilesFinder from "./test-files-finder.js"
3
- import TestRunner from "./test-runner.js"
1
+ import BaseCommand from "../base-command.js"
2
+ import TestFilesFinder from "../../testing/test-files-finder.js"
3
+ import TestRunner from "../../testing/test-runner.js"
4
4
 
5
5
  export default class VelociousCliCommandsInit extends BaseCommand {
6
6
  async execute() {
7
7
  const testFilesFinder = new TestFilesFinder({directory: this.directory(), processArgs: this.processArgs})
8
8
  const testFiles = await testFilesFinder.findTestFiles()
9
-
10
- const testRunner = new TestRunner(testFiles)
9
+ const testRunner = new TestRunner({configuration: this.configuration, testFiles})
11
10
 
12
11
  await testRunner.run()
13
12
  }
package/src/controller.js CHANGED
@@ -1,9 +1,16 @@
1
1
  import {digs} from "diggerize"
2
2
  import ejs from "ejs"
3
3
  import * as inflection from "inflection"
4
+ import logger from "./logger.js"
4
5
  import restArgsError from "./utils/rest-args-error.js"
5
6
 
6
7
  export default class VelociousController {
8
+ static beforeAction(methodName) {
9
+ if (!this._beforeActions) this._beforeActions = []
10
+
11
+ this._beforeActions.push(methodName)
12
+ }
13
+
7
14
  constructor({action, configuration, controller, params, request, response, viewPath}) {
8
15
  if (!action) throw new Error("No action given")
9
16
  if (!configuration) throw new Error("No configuration given")
@@ -23,6 +30,36 @@ export default class VelociousController {
23
30
  this._viewPath = viewPath
24
31
  }
25
32
 
33
+ async _runBeforeCallbacks() {
34
+ await logger(this, "_runBeforeCallbacks")
35
+
36
+ let currentControllerClass = this.constructor
37
+
38
+ while (currentControllerClass) {
39
+ await logger(this, `Running callbacks for ${currentControllerClass.name}`)
40
+
41
+ const beforeActions = currentControllerClass._beforeActions
42
+
43
+ if (beforeActions) {
44
+ for (const beforeActionName of beforeActions) {
45
+ const beforeAction = currentControllerClass.prototype[beforeActionName]
46
+
47
+ if (!beforeAction) throw new Error(`No such before action: ${beforeActionName}`)
48
+
49
+ const boundBeforeAction = beforeAction.bind(this)
50
+
51
+ await boundBeforeAction()
52
+ }
53
+ }
54
+
55
+ currentControllerClass = Object.getPrototypeOf(currentControllerClass)
56
+
57
+ if (!currentControllerClass?.name?.endsWith("Controller")) break
58
+ }
59
+
60
+ await logger(this, "After runBeforeCallbacks")
61
+ }
62
+
26
63
  params = () => this._params
27
64
 
28
65
  render({json, status, ...restArgs} = {}) {
@@ -13,16 +13,27 @@ export default class VelociousDatabaseMigrateFromRequireContext {
13
13
 
14
14
  const files = requireContext.keys()
15
15
  .map((file) => {
16
- const match = file.match(/(\d{14})-(.+)\.js$/)
16
+ // "13,14" because somes "require-context"-npm-module deletes first character!?
17
+ const match = file.match(/(\d{13,14})-(.+)\.js$/)
17
18
 
18
19
  if (!match) return null
19
20
 
20
- const date = parseInt(match[1])
21
+ // Fix require-context-npm-module deletes first character
22
+ let fileName = file
23
+ let dateNumber = match[1]
24
+
25
+ if (dateNumber.length == 13) {
26
+ dateNumber = `2${dateNumber}`
27
+ fileName = `2${fileName}`
28
+ }
29
+
30
+ // Parse regex
31
+ const date = parseInt(dateNumber)
21
32
  const migrationName = match[2]
22
33
  const migrationClassName = inflection.camelize(migrationName.replaceAll("-", "_"))
23
34
 
24
35
  return {
25
- file,
36
+ file: fileName,
26
37
  date,
27
38
  migrationClassName
28
39
  }
@@ -40,6 +40,7 @@ export default class VeoliciousHttpServerClient {
40
40
  this.currentRequest = new Request({
41
41
  configuration: this.configuration
42
42
  })
43
+
43
44
  this.currentRequest.requestParser.events.on("done", this.executeCurrentRequest)
44
45
  this.currentRequest.feed(data)
45
46
  this.state = "requestStarted"
@@ -152,12 +152,12 @@ export default class RequestBuffer {
152
152
 
153
153
  this.events.emit("header", header)
154
154
  } else if (line == "\r\n") {
155
- if (this.httpMethod.toUpperCase() == "GET") {
155
+ if (this.httpMethod.toUpperCase() == "GET" || this.httpMethod.toUpperCase() == "OPTIONS") {
156
156
  this.completeRequest()
157
157
  } else if (this.httpMethod.toUpperCase() == "POST") {
158
158
  this.readingBody = true
159
159
 
160
- const match = this.getHeader("content-type").value.match(/^multipart\/form-data;\s*boundary=(.+)$/i)
160
+ const match = this.getHeader("content-type")?.value?.match(/^multipart\/form-data;\s*boundary=(.+)$/i)
161
161
 
162
162
  if (match) {
163
163
  this.boundary = match[1]
@@ -176,7 +176,7 @@ export default class RequestBuffer {
176
176
  }
177
177
 
178
178
  parseStatusLine(line) {
179
- const match = line.match(/^(GET|POST) (.+?) HTTP\/1\.1\r\n/)
179
+ const match = line.match(/^(GET|OPTIONS|POST) (.+?) HTTP\/1\.1\r\n/)
180
180
 
181
181
  if (!match) {
182
182
  throw new Error(`Couldn't match status line from: ${line}`)
@@ -185,6 +185,8 @@ export default class RequestBuffer {
185
185
  this.httpMethod = match[1]
186
186
  this.path = match[2]
187
187
  this.setState("headers")
188
+
189
+ logger(this, () => ["Parsed status line", {httpMethod: this.httpMethod, path: this.path}])
188
190
  }
189
191
 
190
192
  postRequestDone() {
@@ -194,7 +196,7 @@ export default class RequestBuffer {
194
196
  }
195
197
 
196
198
  setState(newState) {
197
- logger(this, `Changing state from ${this.state} to ${newState}`)
199
+ logger(this, () => [`Changing state from ${this.state} to ${newState}`])
198
200
 
199
201
  this.state = newState
200
202
  }
@@ -34,9 +34,46 @@ export default class VelociousHttpServerClientRequestParser {
34
34
  feed = (data) => this.requestBuffer.feed(data)
35
35
  getHeader = (name) => this.requestBuffer.getHeader(name)?.value
36
36
  getHttpMethod = () => digg(this, "requestBuffer", "httpMethod")
37
- getHost = () => this.requestBuffer.getHeader("host")?.value
37
+
38
+ _getHostMatch = () => {
39
+ const rawHost = this.requestBuffer.getHeader("origin")?.value
40
+
41
+ if (!rawHost) return null
42
+
43
+ const match = rawHost.match(/^(.+):\/\/(.+)(|:(\d+))/)
44
+
45
+ if (!match) throw new Error(`Couldn't match host: ${rawHost}`)
46
+
47
+ return {
48
+ protocol: match[1],
49
+ host: match[2],
50
+ port: match[4]
51
+ }
52
+ }
53
+
54
+ getHost() {
55
+ const rawHostSplit = this.requestBuffer.getHeader("host")?.value?.split(":")
56
+
57
+ if (rawHostSplit && rawHostSplit[0]) return rawHostSplit[0]
58
+ }
59
+
38
60
  getPath = () => digg(this, "requestBuffer", "path")
39
61
 
62
+ getPort() {
63
+ const rawHostSplit = this.requestBuffer.getHeader("host")?.value?.split(":")
64
+ const httpMethod = this.getHttpMethod()
65
+
66
+ if (rawHostSplit && rawHostSplit[1]) {
67
+ return parseInt(rawHostSplit[1])
68
+ } else if (httpMethod == "http") {
69
+ return 80
70
+ } else if (httpMethod == "https") {
71
+ return 443
72
+ }
73
+ }
74
+
75
+ getProtocol = () => this._getHostMatch()?.protocol
76
+
40
77
  requestDone = () => {
41
78
  const incorporator = new Incorporator({objects: [this.params, this.requestBuffer.params]})
42
79
 
@@ -19,17 +19,27 @@ export default class VelociousHttpServerClientRequestRunner {
19
19
  getState = () => this.state
20
20
 
21
21
  async run() {
22
- if (!this.request) throw new Error("No request?")
22
+ const {configuration, request, response} = this
23
23
 
24
- logger(this, "Run request")
24
+ if (!request) throw new Error("No request?")
25
25
 
26
- const routesResolver = new RoutesResolver({
27
- configuration: this.configuration,
28
- request: this.request,
29
- response: this.response
30
- })
26
+ try {
27
+ if (request.httpMethod() == "OPTIONS" && request.header("sec-fetch-mode") == "cors") {
28
+ await logger(this, () => ["Run CORS", {httpMethod: request.httpMethod(), secFetchMode: request.header("sec-fetch-mode")}])
29
+ await configuration.cors({request, response})
30
+ } else {
31
+ await logger(this, "Run request")
32
+ const routesResolver = new RoutesResolver({configuration, request, response})
33
+
34
+ await routesResolver.resolve()
35
+ }
36
+ } catch (error) {
37
+ await logger(this, `Error while running request: ${error.message}`)
38
+
39
+ response.setStatus(500)
40
+ response.setErrorBody(error)
41
+ }
31
42
 
32
- await routesResolver.resolve()
33
43
  this.state = "done"
34
44
  this.events.emit("done", this)
35
45
  }
@@ -7,9 +7,31 @@ export default class VelociousHttpServerClientRequest {
7
7
  this.requestParser = new RequestParser({configuration})
8
8
  }
9
9
 
10
+ baseURL = () => `${this.protocol()}://${this.hostWithPort()}`
10
11
  feed = (data) => this.requestParser.feed(data)
12
+ header = (headerName) => this.requestParser.requestBuffer.getHeader(headerName)?.value
11
13
  httpMethod = () => this.requestParser.getHttpMethod()
12
14
  host = () => this.requestParser.getHost()
15
+
16
+ hostWithPort = () => {
17
+ const port = this.port()
18
+ const protocol = this.protocol()
19
+ let hostWithPort = `${this.host()}`
20
+
21
+ if (port == 80 && protocol == "http") {
22
+ // Do nothing
23
+ } else if (port == 443 && protocol == "https") {
24
+ // Do nothing
25
+ } else if (port) {
26
+ hostWithPort += `:${port}`
27
+ }
28
+
29
+ return hostWithPort
30
+ }
31
+
32
+ origin = () => this.header("origin")
13
33
  path = () => this.requestParser.getPath()
14
34
  params = () => digg(this, "requestParser", "params")
35
+ port = () => this.requestParser.getPort()
36
+ protocol = () => this.requestParser.getProtocol()
15
37
  }
@@ -15,7 +15,7 @@ export default class VelociousHttpServerClientResponse {
15
15
  }
16
16
 
17
17
  getBody() {
18
- if (this.body) {
18
+ if (this.body !== undefined) {
19
19
  return this.body
20
20
  }
21
21
 
@@ -34,10 +34,21 @@ export default class VelociousHttpServerClientResponse {
34
34
  this.body = value
35
35
  }
36
36
 
37
+ setErrorBody(error) {
38
+ this.body = `${error.message}\n\n${error.stack}`
39
+ this.addHeader("Content-Type", "text/plain")
40
+ }
41
+
37
42
  setStatus(status) {
38
- if (status == "not-found" || status == 404) {
43
+ if (status == "success" || status == 200) {
44
+ this.statusCode = 200
45
+ this.statusMessage = "OK"
46
+ } else if (status == "not-found" || status == 404) {
39
47
  this.statusCode = 404
40
48
  this.statusMessage = "Not Found"
49
+ } else if (status == "internal-server-error" || status == 500) {
50
+ this.statusCode = 500
51
+ this.statusMessage = "Internal server error"
41
52
  } else {
42
53
  throw new Error(`Unhandled status: ${status}`)
43
54
  }
@@ -1,4 +1,5 @@
1
1
  import {digg} from "diggerize"
2
+ import EventEmitter from "events"
2
3
  import logger from "../logger.js"
3
4
  import Net from "net"
4
5
  import ServerClient from "./server-client.js"
@@ -7,6 +8,7 @@ import WorkerHandler from "./worker-handler/index.js"
7
8
  export default class VelociousHttpServer {
8
9
  clientCount = 0
9
10
  clients = {}
11
+ events = new EventEmitter()
10
12
  workerCount = 0
11
13
  workerHandlers = []
12
14
 
@@ -20,6 +22,7 @@ export default class VelociousHttpServer {
20
22
  async start() {
21
23
  await this._ensureAtLeastOneWorker()
22
24
  this.netServer = new Net.Server()
25
+ this.netServer.on("close", this.onClose)
23
26
  this.netServer.on("connection", this.onConnection)
24
27
  await this._netServerListen()
25
28
  }
@@ -72,6 +75,10 @@ export default class VelociousHttpServer {
72
75
  await this.stopServer()
73
76
  }
74
77
 
78
+ onClose = () => {
79
+ this.events.emit("close")
80
+ }
81
+
75
82
  onConnection = (socket) => {
76
83
  const clientCount = this.clientCount
77
84
 
@@ -25,7 +25,7 @@ export default class ServerClient {
25
25
  }
26
26
 
27
27
  onSocketData = (chunk) => {
28
- logger(this, `Socket ${this.clientCount}: ${chunk}`)
28
+ logger(this, () => [`Socket ${this.clientCount}: ${chunk}`])
29
29
 
30
30
  this.worker.postMessage({
31
31
  command: "clientWrite",
@@ -54,6 +54,8 @@ export default class VelociousHttpServerWorker {
54
54
  onWorkerExit = (code) => {
55
55
  if (code !== 0) {
56
56
  throw new Error(`Client worker stopped with exit code ${code}`)
57
+ } else {
58
+ logger(this, () => [`Client worker stopped with exit code ${code}`])
57
59
  }
58
60
  }
59
61
 
@@ -66,12 +68,10 @@ export default class VelociousHttpServerWorker {
66
68
  this.onStartCallback()
67
69
  this.onStartCallback = null
68
70
  } else if (command == "clientOutput") {
69
- logger(this, "CLIENT OUTPUT", data)
71
+ logger(this, () => ["CLIENT OUTPUT", data])
70
72
 
71
73
  const {clientCount, output} = digs(data, "clientCount", "output")
72
74
 
73
- logger(this, "CLIENT OUTPUT", data)
74
-
75
75
  this.clients[clientCount].send(output)
76
76
  } else {
77
77
  throw new Error(`Unknown command: ${command}`)
@@ -38,10 +38,10 @@ export default class VelociousHttpServerWorkerHandlerWorkerThread {
38
38
  }
39
39
  }
40
40
 
41
- onCommand = (data) => {
42
- logger(this, `Worker ${this.workerCount} received command`, data)
41
+ onCommand = async (data) => {
42
+ await logger(this, () => [`Worker ${this.workerCount} received command`, data])
43
43
 
44
- const {command} = data
44
+ const command = data.command
45
45
 
46
46
  if (command == "newClient") {
47
47
  const {clientCount} = digs(data, "clientCount")
@@ -56,9 +56,13 @@ export default class VelociousHttpServerWorkerHandlerWorkerThread {
56
56
 
57
57
  this.clients[clientCount] = client
58
58
  } else if (command == "clientWrite") {
59
+ await logger(this, "Looking up client")
60
+
59
61
  const {chunk, clientCount} = digs(data, "chunk", "clientCount")
60
62
  const client = digg(this.clients, clientCount)
61
63
 
64
+ await logger(this, `Sending to client ${clientCount}`)
65
+
62
66
  client.onWrite(chunk)
63
67
  } else {
64
68
  throw new Error(`Unknown command: ${command}`)
package/src/logger.js CHANGED
@@ -1,15 +1,51 @@
1
- import {digg} from "diggerize"
1
+ import Configuration from "./configuration.js"
2
2
 
3
- export default function log(object, ...messages) {
4
- if (!object.configuration) console.error(`No configuration on ${object.constructor.name}`)
3
+ function consoleLog(message) {
4
+ return new Promise((resolve) => {
5
+ process.stdout.write(message, "utf8", resolve)
6
+ })
7
+ }
5
8
 
6
- if (object.configuration?.debug) {
7
- if (!object.constructor.name) {
8
- throw new Error(`No constructor name for object`)
9
- }
9
+ export default async function log(object, ...messages) {
10
+ let configuration
11
+
12
+ if (object.configuration) {
13
+ configuration = object.configuration
14
+ } else {
15
+ configuration = Configuration.current()
16
+ }
17
+
18
+ if (configuration?.debug) {
19
+ try {
20
+ if (!object.constructor.name) {
21
+ throw new Error(`No constructor name for object`)
22
+ }
23
+
24
+ const className = object.constructor.name
10
25
 
11
- const className = object.constructor.name
26
+ if (messages.length === 1 && typeof messages[0] == "function") {
27
+ messages = messages[0]()
28
+ }
12
29
 
13
- console.log(className, ...messages)
30
+ let message = ""
31
+
32
+ for (const messagePartIndex in messages) {
33
+ const messagePart = messages[messagePartIndex]
34
+
35
+ if (messagePartIndex > 0) {
36
+ message += " "
37
+ }
38
+
39
+ if (typeof messagePart == "object") {
40
+ message += JSON.stringify(messagePart)
41
+ } else {
42
+ message += messagePart
43
+ }
44
+ }
45
+
46
+ await consoleLog(`${className} ${message}\n`)
47
+ } catch (error) {
48
+ console.error(`ERROR ${error.message}`)
49
+ }
14
50
  }
15
51
  }
@@ -10,7 +10,7 @@ export default class VelociousRouteGetRoute extends BaseRoute {
10
10
  this.regExp = new RegExp(`^(${escapeStringRegexp(name)})(.*)$`)
11
11
  }
12
12
 
13
- matchWithPath(path) {
13
+ matchWithPath({path}) {
14
14
  if (path.match(this.regExp)) {
15
15
  const [_beginnigSlash, _matchedName, restPath] = match
16
16
 
@@ -57,6 +57,7 @@ export default class VelociousRoutesResolver {
57
57
  }
58
58
 
59
59
  await this.configuration.getDatabasePool().withConnection(async () => {
60
+ await controllerInstance._runBeforeCallbacks()
60
61
  await controllerInstance[action]()
61
62
  })
62
63
  }
@@ -20,7 +20,7 @@ export default class VelociousRouteResourceRoute extends BaseRoute {
20
20
  let subRoutesMatchesRestPath = false
21
21
 
22
22
  for (const route of this.routes) {
23
- if (route.matchWithPath(restPath)) {
23
+ if (route.matchWithPath({path: restPath})) {
24
24
  subRoutesMatchesRestPath = true
25
25
  }
26
26
  }
@@ -0,0 +1,42 @@
1
+ class Response {
2
+ constructor(fetchResponse) {
3
+ this.fetchResponse = fetchResponse
4
+ }
5
+
6
+ async parse() {
7
+ this._body = await this.fetchResponse.text()
8
+ }
9
+
10
+ body = () => this._body
11
+ contentType = () => this.fetchResponse.headers.get("content-type")
12
+ statusCode = () => this.fetchResponse.status
13
+ }
14
+
15
+ export default class RequestClient {
16
+ host = "localhost"
17
+ port = 31006
18
+
19
+ get() {
20
+ throw new Error("get stub")
21
+ }
22
+
23
+ async post(path, data) {
24
+ const fetchResponse = await fetch(
25
+ `http://${this.host}:${this.port}${path}`,
26
+ {
27
+ body: JSON.stringify(data),
28
+ headers: {
29
+ "Content-Type": "application/json"
30
+ },
31
+ method: "POST",
32
+ signal: AbortSignal.timeout(5000)
33
+ }
34
+ )
35
+
36
+ const response = new Response(fetchResponse)
37
+
38
+ await response.parse()
39
+
40
+ return response
41
+ }
42
+ }
@@ -90,7 +90,7 @@ export default class TestFilesFinder {
90
90
  return true
91
91
  }
92
92
  }
93
- } else if (file.match(/-spec\.js/)) {
93
+ } else if (file.match(/-(spec|test)\.js/)) {
94
94
  return true
95
95
  }
96
96
 
@@ -0,0 +1,85 @@
1
+ import Application from "../../src/application.js"
2
+ import RequestClient from "./request-client.js"
3
+ import {tests} from "./test.js"
4
+
5
+ export default class TestRunner {
6
+ constructor({configuration, testFiles}) {
7
+ this.configuration = configuration
8
+ this.testFiles = testFiles
9
+ }
10
+
11
+ async application() {
12
+ if (!this._application) {
13
+ this._application = new Application({
14
+ configuration: this.configuration,
15
+ databases: {
16
+ default: {
17
+ host: "mysql",
18
+ username: "user",
19
+ password: ""
20
+ }
21
+ },
22
+ httpServer: {port: 31006}
23
+ })
24
+
25
+ await this._application.initialize()
26
+ await this._application.startHttpServer()
27
+ }
28
+
29
+ return this._application
30
+ }
31
+
32
+ async requestClient() {
33
+ if (!this._requestClient) {
34
+ this._requestClient = new RequestClient()
35
+ }
36
+
37
+ return this._requestClient
38
+ }
39
+
40
+ async importTestFiles() {
41
+ for (const testFile of this.testFiles) {
42
+ const importTestFile = await import(testFile)
43
+ }
44
+ }
45
+
46
+ async run() {
47
+ await this.importTestFiles()
48
+ await this.runTests(tests, [], 0)
49
+ }
50
+
51
+ async runTests(tests, descriptions, indentLevel) {
52
+ const leftPadding = " ".repeat(indentLevel * 2)
53
+
54
+ for (const testDescription in tests.tests) {
55
+ const testData = tests.tests[testDescription]
56
+ const testArgs = Object.assign({}, testData.args)
57
+ const testName = descriptions.concat([`it ${testDescription}`]).join(" - ")
58
+
59
+ if (testArgs.type == "request") {
60
+ testArgs.application = await this.application()
61
+ testArgs.client = await this.requestClient()
62
+ }
63
+
64
+ console.log(`${leftPadding}it ${testDescription}`)
65
+
66
+ try {
67
+ await testData.function(testArgs)
68
+ } catch (error) {
69
+ // console.error(`${leftPadding} Test failed: ${error.message}`)
70
+ console.error(error.stack)
71
+ }
72
+ }
73
+
74
+ await this.configuration.getDatabasePool().withConnection(async () => {
75
+ for (const subDescription in tests.subs) {
76
+ const subTest = tests.subs[subDescription]
77
+ const newDecriptions = descriptions.concat([subDescription])
78
+
79
+ console.log(`${leftPadding}${subDescription}`)
80
+
81
+ await this.runTests(subTest, newDecriptions, indentLevel + 1)
82
+ }
83
+ })
84
+ }
85
+ }
@@ -0,0 +1,92 @@
1
+ const tests = {
2
+ args: {},
3
+ subs: {},
4
+ tests: {}
5
+ }
6
+
7
+ let currentPath = [tests]
8
+
9
+ class Expect {
10
+ constructor(object) {
11
+ this._object = object
12
+ }
13
+
14
+ toEqual(result) {
15
+ if (this._object != result) {
16
+ throw new Error(`${this._object} wasn't equal to ${result}`)
17
+ }
18
+ }
19
+
20
+ toHaveAttributes(result) {
21
+ const differences = {}
22
+
23
+ for (const key in result) {
24
+ const value = result[key]
25
+ const objectValue = this._object[key]()
26
+
27
+ if (value != objectValue) {
28
+ differences[key] = [value, objectValue]
29
+ }
30
+ }
31
+
32
+ if (Object.keys(differences).length > 0)
33
+ throw new Error(`Object had differet values: ${JSON.stringify(differences)}`)
34
+ }
35
+ }
36
+
37
+ async function describe(description, arg1, arg2) {
38
+ let testArgs, testFunction
39
+
40
+ if (typeof arg2 == "function") {
41
+ testFunction = arg2
42
+ testArgs = arg1
43
+ } else if (typeof arg1 == "function") {
44
+ testFunction = arg1
45
+ testArgs = {}
46
+ } else {
47
+ throw new Error(`Invalid arguments for describe: ${arg1}, ${arg2}`)
48
+ }
49
+
50
+ const currentTest = currentPath[currentPath.length - 1]
51
+ const newTestArgs = Object.assign({}, currentTest.args, testArgs)
52
+
53
+ if (description in currentTest.subs) {
54
+ throw new Error(`Duplicate test description: ${description}`)
55
+ }
56
+
57
+ const newTestData = {args: newTestArgs, subs: {}, tests: {}}
58
+
59
+ currentTest.subs[description] = newTestData
60
+ currentPath.push(newTestData)
61
+
62
+ try {
63
+ await testFunction()
64
+ } finally {
65
+ currentPath.pop()
66
+ }
67
+ }
68
+
69
+ function expect(arg) {
70
+ return new Expect(arg)
71
+ }
72
+
73
+ function it(description, arg1, arg2) {
74
+ const currentTest = currentPath[currentPath.length - 1]
75
+ let testArgs, testFunction
76
+
77
+ if (typeof arg1 == "function") {
78
+ testFunction = arg1
79
+ testArgs = {}
80
+ } else if (typeof arg2 == "function") {
81
+ testFunction = arg2
82
+ testArgs = arg1
83
+ } else {
84
+ throw new Error(`Invalid arguments for it: ${description}, ${arg1}`)
85
+ }
86
+
87
+ const newTestArgs = Object.assign({}, currentTest.args, testArgs)
88
+
89
+ currentTest.tests[description] = {args: newTestArgs, function: testFunction}
90
+ }
91
+
92
+ export {describe, expect, it, tests}
@@ -1,19 +0,0 @@
1
- export default class TestRunner {
2
- constructor(testFiles) {
3
- this.testFiles = testFiles
4
- }
5
-
6
- async importTestFiles() {
7
- for (const testFile of this.testFiles) {
8
- const importTestFile = await import(testFile)
9
- }
10
- }
11
-
12
- async run() {
13
- await this.importTestFiles()
14
-
15
- console.log({foundTestFiles: this.testFiles})
16
-
17
- throw new Error("stub")
18
- }
19
- }