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 +79 -2
- package/bin/velocious.js +17 -1
- package/package.json +1 -1
- package/spec/cli/commands/test/test-files-finder-spec.js +1 -1
- package/spec/http-server/get-spec.js +13 -0
- package/src/application.js +15 -0
- package/src/cli/commands/server.js +5 -6
- package/src/cli/commands/{test/index.js → test.js} +4 -5
- package/src/controller.js +37 -0
- package/src/database/migrate-from-require-context.js +14 -3
- package/src/http-server/client/index.js +1 -0
- package/src/http-server/client/request-buffer/index.js +6 -4
- package/src/http-server/client/request-parser.js +38 -1
- package/src/http-server/client/request-runner.js +18 -8
- package/src/http-server/client/request.js +22 -0
- package/src/http-server/client/response.js +13 -2
- package/src/http-server/index.js +7 -0
- package/src/http-server/server-client.js +1 -1
- package/src/http-server/worker-handler/index.js +3 -3
- package/src/http-server/worker-handler/worker-thread.js +7 -3
- package/src/logger.js +45 -9
- package/src/routes/get-route.js +1 -1
- package/src/routes/resolver.js +1 -0
- package/src/routes/resource-route.js +1 -1
- package/src/testing/request-client.js +42 -0
- package/src/{cli/commands/test → testing}/test-files-finder.js +1 -1
- package/src/testing/test-runner.js +85 -0
- package/src/testing/test.js +92 -0
- package/src/cli/commands/test/test-runner.js +0 -19
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
|
|
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
|
-
|
|
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
|
|
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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import TestFilesFinder from "../../../../src/
|
|
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
|
})
|
package/src/application.js
CHANGED
|
@@ -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
|
|
15
|
-
const
|
|
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 "
|
|
2
|
-
import TestFilesFinder from "
|
|
3
|
-
import TestRunner from "
|
|
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
|
-
|
|
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
|
-
|
|
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")
|
|
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
|
-
|
|
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
|
-
|
|
22
|
+
const {configuration, request, response} = this
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
if (!request) throw new Error("No request?")
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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 == "
|
|
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
|
}
|
package/src/http-server/index.js
CHANGED
|
@@ -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
|
|
|
@@ -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
|
|
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
|
|
1
|
+
import Configuration from "./configuration.js"
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
function consoleLog(message) {
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
process.stdout.write(message, "utf8", resolve)
|
|
6
|
+
})
|
|
7
|
+
}
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
26
|
+
if (messages.length === 1 && typeof messages[0] == "function") {
|
|
27
|
+
messages = messages[0]()
|
|
28
|
+
}
|
|
12
29
|
|
|
13
|
-
|
|
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
|
}
|
package/src/routes/get-route.js
CHANGED
|
@@ -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
|
|
package/src/routes/resolver.js
CHANGED
|
@@ -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
|
+
}
|
|
@@ -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
|
-
}
|